react-state-editor
v10.0.5
Published
Developer-friendly React state editor
Downloads
8
Readme
react-state-editor
Developer-friendly React state editor
State types limitation
Deprecated object keys
$
,map$
Create store context
// store.ts
import {
createStoreContext,
createUseStorePathfinder,
} from 'react-state-editor';
export type User = {
id: number;
name: string;
};
export type MyInState = {
users: User[];
};
export type MyOutState = {
users: User[];
userLabels: string[];
};
export const MyContext = createStoreContext<MyOutState>('MyContext');
export const useMy = createUseStorePathfinder(MyContext);
Get store pathfinder
Short way using predefined hook
import { useMy } from './store';
// ...
const my = useMy();
Alternative way
import { useStorePathfinder } from 'react-state-editor';
import { MyContext } from './store';
// ...
const my = useStorePathfinder(MyContext);
Store provider
// MyStoreProvider.tsx
import React, { ReactNode, useMemo } from 'react';
import { useStore } from 'react-state-editor';
import { MyContext, MyInState, MyOutState } from './store';
export type MyStoreProviderProps = {
children: ReactNode;
};
export const MyStoreProvider = ({ children }: MyStoreProviderProps) => {
const store = useStore<MyInState, MyOutState>(
() => ({
/* initial state */
users: [
{ id: 0, name: 'Sméagol' },
{ id: 1, name: 'Gollum' },
],
}),
useComputeState,
{ defaultCacheLocation: 'store' }
);
return <MyContext.Provider value={store}>{children}</MyContext.Provider>;
};
const useComputeState = (s: MyInState): MyOutState => {
const userLabels = useMemo(
() => s.users.map((u) => `${s.name} (${s.id})`),
[s.users]
);
return { ...s, userLabels };
};
Demo
// Demo.tsx
export const Demo = () => {
return (
<MyStoreProvider>
<UserList />
<hr />
<UserListEdit />
<hr />
<UserNames />
<hr />
<ValueListDemo />
<hr />
<UsersInfo />
</MyStoreProvider>
);
};
Custom mutation hooks
Mutations should return input state
// mutations.tsx
import { createUseStoreReduce } from 'react-state-editor';
import { MyContext, MyInState } from './store';
export const useAddUser = createUseStoreReduce(
MyContext,
(s, name: string): MyInState => ({
...s,
users: [...s.users, { id: Math.random(), name }],
})
);
export const useRemoveUser = createUseStoreReduce(
MyContext,
(s, id: number): MyInState => ({
...s,
users: s.users.filter((u) => u.id !== id),
})
);
// UserListEdit.tsx
import { useAddUser, useRemoveUser } from './mutations';
export const UserListEdit = () => {
const addUser = useAddUser();
const removeUser = useRemoveUser();
return (
<div>
<button onClick={() => addUser(`A hobbit`)}>Add user</button>
<button onClick={() => removeUser(0)}>Remove user</button>
</div>
);
};
Custom editors
// StringEditor.tsx
import React, { memo, useCallback } from 'react';
import { StateEditorProps, useStoreState } from 'react-state-editor';
export type StringEditorCustomProps = {
isSomeOption: (value: string) => boolean;
};
// Reusable string editor (independent of concrete store)
// Optionally can wrapped with React.memo
export const StringEditor = memo(function TextEditor({
$, // Pathfinder object (comes from StateEditorProps<string>)
isSomeOption,
...props
}: StringEditorCustomProps & StateEditorProps<string>) {
const [state, setState] = useStoreState($); // Use state via pathfinder
const onChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const { value } = e.target;
if (isSomeOption(value)) {
setState(value);
}
},
[setState, validate]
);
return <input value={state} onChange={onChange} />;
});
Use store state
// UserItem.tsx
import React from 'react';
import { StateEditorProps, useStoreState } from 'react-state-editor';
import { useMy } from './store';
import { StringEditor } from './StringEditor';
export type UserItemProps = { index: number };
export const UserItem = ({ index }: UserItemProps) => {
const my = useMy(); // Get root pathfinder
const user$ = my.users[index]; // Get user pathfinder
const [user, setUser] = useStoreState(user$);
// Alternative
// const user = useStoreSelect(user$);
// const setUser = useStoreSet(user$);
const reduceUser = useStoreReduce(user$);
return (
<div>
<StringEditor $={user$.name} isSomeOption={(name) => name.length > 20} />
<button
onClick={() => reduceUser((u) => ({ ...u, name: u.name + '-x' }))}
>
{user.name}
</button>
</div>
);
};
Use array mapping
<array pathfinder>.map$
is special property for mapping item fields
// UserNames.tsx
import React, { useMemo } from 'react';
import { ValueList } from './ValueList';
import { useMy } from './store';
export function UserNames() {
const my = useMy(); // Get root pathfinder
// Reacts on names change only (!!!)
const names = useStoreSelect(my.users.map$.name);
const joinedNames = useMemo(() => names.join(', '), [names]);
return <div>User names: {joinedNames}</div>;
}
Use arrays (use unique field as key)
// UserList.tsx
import { ArrayView } from 'react-state-editor';
import { UserItem } from './UserItem';
import { useMy } from './store';
export const UserList = () => {
const my = useMy(); // Get root pathfinder
return <ArrayView $={my.users.map$.id}>{usersItem}</ArrayView>;
};
const usersItem = (id: string, i: number) => <UserItem key={id} index={i} />;
Use arrays (use index as key)
WARNING: Usage of unique field as key is preferable See Keys
// UserList.tsx
import { ArrayViewIndexKey } from 'react-state-editor';
import { UserItem } from './UserItem';
import { useMy } from './store';
export const UserList = () => {
const my = useMy(); // Get root pathfinder
return <ArrayViewIndexKey $={my.users}>{usersItem}</ArrayViewIndexKey>;
};
const usersItem = (i: number) => <UserItem index={i} />;
Use arrays (map indexes)
// UserList.tsx
import { useStoreSelect, mapIndexes } from 'react-state-editor';
import { UserItem } from './UserItem';
import { useMy } from './store';
export const UserList = () => {
const my = useMy(); // Get root pathfinder
// Reacts on every user property change (!!!)
const users = useStoreSelect(my.users);
// ... Some another usage of users
return (
<>
{mapIndexes(users.length, (i) => (
<UserItem key={i} index={i} />
))}
</>
);
};
Use any value
// UsersInfo.tsx
import { useStoreSelect, mapIndexes } from 'react-state-editor';
import { UserItem } from './UserItem';
import { useMy } from './store';
export const UsersInfo = () => {
const my = useMy(); // Get root pathfinder
return <View $={my}>{usersInfo}</View>;
};
const usersInfo = (my: MyOutState) => <div>Total {my.users.length} users</div>;