@fleur/lys
v3.0.1
Published
lys is an micro statement manager for '21s react
Downloads
46
Readme
Lys
Lys (risu) is an minimal statement manger for '21s React.
It's focus to Per page state management, not application global state management.
Lys is usable to instead of useReducer
, Mobx
, or Recoil
if you have async procedure.
yarn add @fleur/lys
Features
- Per page level micro state management
- Initial state via external data
- Can be use with likes
Next.js
,useSWR
- Can be use with likes
- Testing friendly
- Type safe
- Minimal re-rendering
Usage
Summary in CodeSandbox Example.
First, define your slice.
import { createSlice } from '@fleur/lys';
const formSlice = createSlice({
actions: {
// Define actions
async patchItem({ commit }, index: number, patch: Partial<State['form']['items'][0]>) {
commit((draft) => {
Object.assign(draft.form.items[index], patch);
});
},
async submit({ state, commit }) {
if (state.hasError) return;
commit({ submitting: true });
commit({
submiting: false,
form: await (
await fetch('/api/users', { body: JSON.stringify(state.form) })
).json(),
});
},
async validate({ state }) {
commit({ hasError: false });
// Use your favorite validator
commit({ hasError: await validateForm(state.form) });
},
},
computed: {
// You can define computable values in `computed`
// `computed` is cached between to next state changed
itemOf: (state) => (index: number) => state.form.items[index],
canSubmit: (state) => !state.submitting,
},
}, (): State => ({
// Define initial state
submitting: false,
hasError: false,
form: {
id: null,
username: "",
items: [{ name: "" }],
},
})
);
Next, initialize slice on your page component
import { useLysSliceRoot, useLysSlice } from '@fleur/lys';
export const NewUserPage = () => {
const { data: initialData, error } = useSWR('/users/1', fetcher);
// Initialize slice by `useLysSliceRoot`
// `initialState` in second argument, it shallow override to Slice's initial state.
// `initialData` is re-evaluated when it changes from null or undefined to something else.
//
// Or can you define `fetchUser` in slice and call it in `useEffect()`
const [state, actions] = useLysSliceRoot(
formSlice,
initialData ? { form: initialData } : null
);
const handleChangeName = useCallback(({ currentTarget }) => {
// `set` is builtin action
actions.set((draft) => {
draft.form.username = currentTarget.value;
});
}, []);
const handleSubmit = useCallback(async () => {
await actions.validate();
await actions.submit();
}, []);
return (
<div>
<label>
Display name:
<input type="text" value={state.form.name} onChange={handleChangeName} />
</label>
<h1>Your items</h1>
{state.form.items.map((index) => (
<Item index={index} />
))}
<button disabled={!state.canSubmit} onClick={handleSubmit}>
Register
</button>
</div>
);
};
Use initialize slice into child component
// In child component
const Item = ({ index }) => {
// Use slice from root component by `useLysSlice`
const [state, actions] = useLysSlice(formSlice);
const item = state.itemOf(index);
const handleChangeName = useCallback(({ currentTarget }) => {
// Can call action from child component and share state with root.
// Re-rendering from root (no duplicate re-rendering)
actions.patchItem(index, { name: currentTarget.value });
}, []);
return (
<div>
Item of #{index + 1}
<label>
Name: <input type="text" value={item.name} />
</label>
</div>
);
};
Testing
Lys's Slice is very testable. Let look testing example!
import { instantiateSlice, createSlice } from "@fleur/lys";
// Define (Normally, import from other file)
const slice = createSlice(
{
actions: {
increment({ commit }) {
commit((draft) => draft.count++);
},
},
computed: {
isZero: (state) => state.count === 0,
},
},
() => ({ count: 0, submitting: false })
);
describe("Testing slice", () => {
it("Should increment one", async () => {
// instantiate
const { state, actions } = instantiateSlice(slice);
// Expection
expect(state.current.count).toBe(0);
expect(state.current.isZero).toBe(true);
await actions.increment();
expect(state.current.count).toBe(1);
expect(state.current.isZero).toBe(false);
});
it("mock slice actions (for component testing)", () => {
const actionSpy = jest.fn(({ state }) => (state.count = 10));
const { state, actions } = mockSlice(
slice,
{
/* part of initial state here */
},
{
/* Mock action implementations here */
increment: actionSpy,
}
);
actions.increment();
expect(actionSpy).toBeCalled();
});
});