Skip to content

Instantly share code, notes, and snippets.

@ulexxander
Last active October 1, 2021 18:31
Show Gist options
  • Save ulexxander/ba1063ffeb126e738ac32fb513581f19 to your computer and use it in GitHub Desktop.
Save ulexxander/ba1063ffeb126e738ac32fb513581f19 to your computer and use it in GitHub Desktop.
Testing approach to write effector business logic only using factories.
import {
combine,
createEffect,
createEvent,
createStore,
Effect,
Event,
forward,
sample,
Store,
} from "effector";
import { useStore } from "effector-react";
import { nanoid } from "nanoid";
import React, { createContext, useContext } from "react";
import ReactDOM from "react-dom";
import "./domains/logicInit";
import "./tailwind.css";
// Business logic
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
type Product = {
id: string;
title: string;
price: number;
};
type CartProduct = Product & {
amount: number;
beingDeleted?: boolean;
};
type AddProductPayload = {
product: Product;
amount: number;
};
type CartService = {
$products: Store<CartProduct[]>;
addProduct: Event<AddProductPayload>;
removeProduct: Event<{ id: string }>;
recoverProduct: Event<{ id: string }>;
};
type FormField<State> = {
$value: Store<State>;
set: Event<State>;
reset: Event<void>;
};
function createFormField<State>(defaultState: State): FormField<State> {
const $value = createStore<State>(defaultState);
const set = createEvent<State>();
const reset = createEvent<void>();
$value.on(set, (_, newValue) => newValue).reset(reset);
return {
$value,
set,
reset,
};
}
type NewProductFormState = {
title: string;
price: number | null;
amount: number | null;
};
type NewProductForm = {
title: FormField<string>;
price: FormField<number | null>;
amount: FormField<number | null>;
$fields: Store<NewProductFormState>;
submit: Event<void>;
submitted: Event<NewProductFormState>;
};
function createNewProductForm(): NewProductForm {
const title = createFormField<string>("");
const price = createFormField<number | null>(null);
const amount = createFormField<number | null>(null);
const $fields = combine<NewProductFormState>({
title: title.$value,
price: price.$value,
amount: amount.$value,
});
const submit = createEvent();
const submitted = createEvent<NewProductFormState>();
sample({
clock: submit,
source: $fields,
target: submitted,
});
return {
title,
price,
amount,
$fields,
submit,
submitted,
};
}
function createCartService(newProductForm: NewProductForm): CartService {
const $products = createStore<CartProduct[]>([]);
const addProduct = createEvent<AddProductPayload>();
const removeProduct = createEvent<{ id: string }>();
const recoverProduct = createEvent<{ id: string }>();
addProduct.watch(() => {
console.log("add product");
});
$products
.on(addProduct, (products, { product, amount }) => [
...products,
{ ...product, amount },
])
.on(removeProduct, (products, { id }) =>
products.map((product) =>
product.id === id ? { ...product, beingDeleted: true } : product
)
)
.on(recoverProduct, (products, { id }) =>
products.map((product) =>
product.id === id ? { ...product, beingDeleted: false } : product
)
);
// adding fake product for now :)
forward({
from: newProductForm.submitted,
to: addProduct.prepend((form) => ({
product: {
id: nanoid(),
title: form.title,
price: form.price || 0,
},
amount: form.amount || 0,
})),
});
return {
$products,
addProduct,
removeProduct,
recoverProduct,
};
}
type Order = {
id: string;
cartProducts: CartProduct[];
};
type AddOrderPayload = {
cartProducts: CartProduct[];
};
type OrderService = {
$orders: Store<Order[] | null>;
loadOrders: Event<void>;
sendOrder: Event<void>;
getOrdersFx: Effect<void, Order[]>;
postOrderFx: Effect<AddOrderPayload, { id: string }>;
};
function createOrderService(cartService: CartService): OrderService {
const $orders = createStore<Order[] | null>(null);
const loadOrders = createEvent();
const sendOrder = createEvent();
const getOrdersFx = createEffect<void, Order[]>(async () => {
await sleep(100);
const orders: Order[] = [
{
id: nanoid(),
cartProducts: [
{
id: nanoid(),
title: "Abc product",
price: 100,
amount: 5,
},
],
},
];
return orders;
});
const postOrderFx = createEffect<AddOrderPayload, { id: string }>(
async () => {
await sleep(50);
const result: { id: string } = {
id: nanoid(),
};
return result;
}
);
$orders.on(getOrdersFx.doneData, (_, orders) => orders);
forward({
from: [loadOrders, postOrderFx.done],
to: getOrdersFx,
});
sample({
clock: sendOrder,
source: cartService.$products,
fn(cartProducts) {
const order: AddOrderPayload = {
cartProducts,
};
return order;
},
target: postOrderFx,
});
return {
$orders,
loadOrders,
sendOrder,
getOrdersFx,
postOrderFx,
};
}
// Providers for business logic inside view layer
const newProductForm = createNewProductForm();
const cartService = createCartService(newProductForm);
const orderService = createOrderService(cartService);
const newProductFormContext = createContext(newProductForm);
const cartServiceContext = createContext(cartService);
const orderServiceContext = createContext(orderService);
function useNewProductForm() {
return useContext(newProductFormContext);
}
function useCartService() {
return useContext(cartServiceContext);
}
function useOrderService() {
return useContext(orderServiceContext);
}
const NewProductFormProvider: React.FC = ({ children }) => (
<newProductFormContext.Provider value={newProductForm}>
{children}
</newProductFormContext.Provider>
);
const CartServiceProvider: React.FC = ({ children }) => (
<cartServiceContext.Provider value={cartService}>
{children}
</cartServiceContext.Provider>
);
const OrderServiceProvider: React.FC = ({ children }) => (
<orderServiceContext.Provider value={orderService}>
{children}
</orderServiceContext.Provider>
);
// View layer
const CartProducts: React.FC = () => {
const cart = useCartService();
const products = useStore(cart.$products);
if (!products.length) {
return <p>Cart is empty</p>;
}
return (
<ul className="space-y-4">
{products.map((product) => (
<li key={product.id}>
<h4>{product.title}</h4>
<p>
${product.price} x {product.amount}
</p>
</li>
))}
</ul>
);
};
function numberOrZero(value: number) {
console.log("number or zero");
return Number.isNaN(value) ? 0 : value;
}
const AddCartProduct: React.FC = () => {
const form = useNewProductForm();
const { title, price, amount } = useStore(form.$fields);
return (
<form
className="flex flex-col items-start space-y-4"
onSubmit={(e) => {
e.preventDefault();
form.submit();
}}
>
<input
type="text"
className="text-white bg-gray-700 border-gray-500"
value={title}
onChange={(e) => form.title.set(e.target.value)}
placeholder="Title"
/>
<input
type="number"
className="text-white bg-gray-700 border-gray-500"
value={price || ""}
onChange={(e) => form.price.set(numberOrZero(e.target.valueAsNumber))}
placeholder="Price"
/>
<input
type="number"
className="text-white bg-gray-700 border-gray-500"
value={amount || ""}
onChange={(e) => form.amount.set(numberOrZero(e.target.valueAsNumber))}
placeholder="Amount"
/>
<button type="submit">Add product</button>
</form>
);
};
const Cart: React.FC = () => {
return (
<section>
<h2>Cart</h2>
<div className="mt-4">
<CartProducts />
</div>
<h3 className="mt-8">Add cart product</h3>
<div className="mt-4">
<AddCartProduct />
</div>
</section>
);
};
const EffectorServices: React.FC = () => {
return (
<div className="w-full min-h-screen text-white bg-gray-900">
<main className="p-8">
<Cart />
</main>
</div>
);
};
ReactDOM.render(
<React.StrictMode>
<CartServiceProvider>
<OrderServiceProvider>
<EffectorServices />
</OrderServiceProvider>
</CartServiceProvider>
</React.StrictMode>,
document.getElementById("root")
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment