The state management library you've been waiting for
Redux is a great tool and undoubtedly helped push the web forward by providing a strong mental model around global state. Redux, however, does come with its flaws:
- Repetition. The boilerplate, just for the most simple task, has become wearisome.
- Distributed logic. Business logic is spread out. Some logic lives in the action, some in the reducer, and some in the component.
- Middleware. Apart from logging and handling promises, middleware ends up being a hassle and makes debugging terribly difficult.
Some other state management solutions in the wild have attempted to solve these issues. Two to note, and mainly where inspiration for Synaptik was drawn, are Unstated and Statty. Both use a basic component with render props to access global state ❤️ - but each had shortcomings.
Unstated's Container
works well to control business logic and encapsulate several pieces of state, all while feeling very familiar to Component local state. Containers, otherwise known as Stores in synaptik, live on, only accessed differently on render.
Statty's approach is great - by using a select
prop to pluck only the pieces of state you want, it's easy to inject derived state as a render prop, while also making it easy to check for referential equality to prevent unecessary renders. Statty was only missing a dedicated logic unit.
Thus, Synaptik was born - marrying the concepts of Unstated and Statty in what looks to be a happy union. Hope y'all like it! 😎
Begin by creating your first store, which extends from Store
.
import { Store } from 'synaptik';
import { Stores } from 'app/lib/synaptik';
interface TodoState {
input: string;
entries: string[];
}
export default class TodoStore extends Store<TodoState, Stores> {
state = {
input: '',
entries: [],
};
updateInput = (input: string) => {
this.setState({ input });
};
addTodo = (todo: string) => {
// setState acts like React component's setState,
// except that it runs synchronously
this.setState({
entries: [...this.state.entries, todo],
});
};
}
Next, create a file that will export your store Provider
and useSynapse
hook:
// app/lib/synaptik.ts
import { createSynaptik } from 'synaptik';
import * as stores from './stores';
const { Provider, useSynapse } = createSynaptik(stores);
export type Stores = typeof stores;
export { Provider, useSynapse };
Lastly, wrap your application with the Provider
.
import { render } from 'react-dom';
import { Provider } from 'app/lib/synaptik';
/*
`stores` look something like the following. The keys are used as
identifier's when accessing the store during render.
{
todos: TodoStore,
...etc
}
*/
const App = () => (
<Provider>
<Entry />
</Provider>
);
render(<App />, window.root);
Now, you can access your stores and state with the useSynapse
hook:
NOTE: if your selector functions returns non-primitive values in an array, you must mark the array
as const
for Typescript to properly infer the signatures of the destructured elements
import { useSynapse } from 'app/lib/synaptik';
function TodoList() {
const [todos, input, updateInput, addTodo] = useSynapse(({ todos }) => [
todos.state.entries,
todos.state.input,
todos.updateInput,
todos.addTodo
]);
return (
<>
<ul>
{todos.map(todo => (
<li>{todo}</li>
))}
</ul>
<form onSubmit={addTodo}>
<input value={input} onChange={updateInput} />
<button type="submit">Submit</button>
</form>
</>
);
}
🚀 You've done it! You have your first todo app up and running 4 simple steps.