Intro & Motivation
Ever since the advent of Redux, many libraries have rushed in to address its ergonomic shortcomings by co-locating state, actions, and state update logic:
// Redux Toolkit
export const todosSlice = createSlice({
name: "todos",
initialState:[],
reducers:{
addTask: (state, action) => {/* state update logic */},
deleteTask: (state, action) => {/* state update logic */},
}
});
// Recoil
function Todo() {
const [todoList, setTodoList] = useRecoilState([]);
const addTask = (todo: Todo) => {/* state update logic */};
const deleteTask = (id: number) => {/* state update logic */};
}
// Zustand
const useStore = create(set => ({
state: [],
addTask: () => {/* state update logic */},
deleteTask: () => {/* state update logic */},
}))
This pattern is great, but it can be improved upon:
- Within each state update function, we find ourselves repeating similar, but not identical, CRUD logic over and over.
- There is an unnecessary layer of abstraction between components and managed state where a function name may contradict its implementation.
- This extra layer of abstraction discourages us from moving component state into the store where it can be more easily tracked.
So how does Olik address these issues? Let's just jump right in…
npm install olik
import { createStore } from 'olik';
const store = createStore({
name: document.title,
state: {
user: { name: '', age: 0 },
todos: new Array<{
id: number; title: string; status: 'todo' | 'done'; urgency: number;
}>()
},
});
Now, for some interactions:
store.user.age.$replace(28);
// { type: 'user.age.replace()', payload: 28 }
store.todos.$insertOne(todo);
// { type: 'todos.insertOne()', payload: { ... } }
store.todos.$find.id.$eq(3).$replace(todo);
// { type: 'todos.find.id.eq(3.replace()', payload: { ... } }}
store.todos.$filter.urgency.$lt(2).$remove();
// { type: 'todos.filter.urgency.lt(2).remove()' }
store.todos.$find.status.$eq(5).status.$replace('done');
// { type: 'todos.find.status.eq(5).status.$replace()', payload: 'done' }
// Read state
const todos = store.todos.$state;
// Listen to state changes to the user's name
const subscription = store.user.name
.$onChange(name => console.log(`name is now "${name}"`))
// Listen to changes to 'pending' todos
store.todos.$filter.status.$eq('pending')
.$onChange(todos => console.log(todos));
// Make an asynchronous update
store.todos
.$replace(() => fetch('https://api.dev/todos').then(r => r.json()));
// Observe state in React (need to install additional dependency: olik-react)
store.todos.$find.id.$eq(3).useState();
// Observe state in Angular (need to install additional dependency: olik-ng)
store.todos.$find.id.$eq(3).observe();
// Observe state in Svelte (need to install additional dependency: olik-svelte)
const todos = $store.todos.$find.id.$eq(3);
Olik is the first state manager to use a completely fluent API.
By chaining together a standard set composable state search and update primitives, we can make surgically precise updates to our immutable state tree with zero ambiguity, in complete type-safety, to any depth, right from our components.
The fluent API allows the library to describe our actions for us with perfect accuracy from within the Devtools extension.
Nested stores are also supported which help us manage and debug component state with or without application state.
If you have all features enabled, this library weighs in at 5kb, however it will arguably have the least effect on your application bundle size because it doesn't require any infrastructural code, immutable updaters, or immutable update helpers (like Immer).
Finally, Olik is not built with any particular framework in mind. So far, it can be used without a framework, with React, Angular, or Svelte.