lys is an micro statement manager for '21s react
[npm-url]: https://www.npmjs.com/package/@fleur/lys
[ci-image-url]: https://img.shields.io/github/workflow/status/fleur-js/lys/CI?logo=github&style=flat-square
[version-image-url]: https://img.shields.io/npm/v/@fleur/lys?style=flat-square
[license-url]: https://opensource.org/licenses/MIT
[license-image]: https://img.shields.io/npm/l/@fleur/lys.svg?style=flat-square
[downloads-image]: https://img.shields.io/npm/dw/@fleur/lys.svg?style=flat-square&logo=npm
[bundlephobia-url]: https://bundlephobia.com/result?p=@fleur/lys@2.0.1
[bundlephobia-image]: https://img.shields.io/bundlephobia/minzip/@fleur/lys?style=flat-square
![CI][ci-image-url] [![latest][version-image-url]][npm-url] [![BundleSize][bundlephobia-image]][bundlephobia-url] [![License][license-image]][license-url] [![npm][downloads-image]][npm-url]
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
- Per page level micro state management
- Initial state via external data
- Can be use with likes Next.js, useSWR
- Testing friendly
- Type safe
- Minimal re-rendering
Summary in CodeSandbox Example.
First, define your slice.
`tsx
import { createSlice } from '@fleur/lys';
const formSlice = createSlice({
actions: {
// Define actions
async patchItem({ commit }, index: number, patch: Partial
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 computedcomputed
// 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
`tsx
import { useLysSliceRoot, useLysSlice } from '@fleur/lys';
export const NewUserPage = () => {
const { data: initialData, error } = useSWR('/users/1', fetcher);
// Initialize slice by useLysSliceRootinitialState
// 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.fetchUser
//
// Or can you define 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 (
Use initialize slice into child component
`tsx
// 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 (
Item of #{index + 1}
);
};
`Testing
Lys's Slice is very testable.
Let look testing example!
`tsx
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();
});
});
``