- Getting started
- Reading from Store
- Writing to Store
- Writing to Store asynchronously
- Redux Devtools Integration
This is a library in the Redux family of state management solutions, featuring a different set of tradeoffs. It is meant to be used with React (or React Native).
- Support Typescript out of the box
- Have React Hooks integrated
- Require as little boilerplate code as possible
- Allow for more flexibility in how to structure code
- No actions (or action creators, or action type constants)
- No reducers
- No immutable state updates, instead we can mutate state directly (thanks to immer)
- Pro: It's dead simple. The API surface is minimal, it's just
get
,subscribe
,update
- Pro: No need to think about how to apply updates to deeply nested state in an immutable way
- Pro: To implement a state update, you only need to write code in one place (with Redux, code for a new action is usually split up in 2 or 3 different places)
- Con: Since the library does not enforce a specific structure, code can get messy in larger projects, unless you are disciplined enough to create your own structure
- Con: No named actions make it hard to debug and monitor where state updates are coming from
- Con: Incompatible with Redux middlewares (but compatible with Redux DevTools)
Probably not. For anything serious, stick to one of the more established solutions.
I'm dogfooding it for my own projects, but there are probably still bugs to be fixed.
Install the dependency:
npm i @bearbytes/karma
Create some type definitions for your state:
interface ITodo {
id: string
title: string
isDone?: boolean
}
interface IAppState {
todos: ITodo[]
}
Create an initial state object:
const initialAppState: IAppState = { todos: [] }
Call createStore
, which also creates typesafe hooks and a Context container. You often don't need everything that function returns, just take out what you need.
import { createStore } from '@bearbytes/karma'
const {
// Global store instance. Most apps only need this one.
store,
// If you want to have multiple store instances in the app,
// this Container can be put into the React component hierarchy
// to create a new store context below it.
Container,
// Get access to the store from the nearest Container,
// or the global instance if no Container is used.
useStore,
// This is the most important hook, which extracts data from the
// store and updates the component whenever that data changes.
useStoreState,
// This hook can be used in a React component to create a callback to
// update state. It has some advantages to calling store.update() directly.
useStoreUpdate,
} = createStore(initialState)
// In most cases, only a single store is needed:
export { store, useStoreState, useStoreUpdate }
Get the current value saved in the store:
const appState = store.get()
Subscribe to the current and all future values, using rxjs:
store.subscribe((s) => {
console.log('AppState is now:', s)
})
Register a listener to a specific piece of data in a React component:
function TodoListComponent() {
// this component will be re-rendered when `todos` are updated,
// but not when a different part of the store is updated
const todos = useStoreState((s) => s.todos)
return (
<>
{todos.map((todo) => (
<TodoListItem key={todo.id} todo={todo} />
))}
</>
)
}
Instead of dispatching an action at one place and having code to update the state in another place, we simply inline this code:
store.update((s) => {
s.todos.push({ id: 123, title: 'Stop writing stupid Todo apps' })
})
You are only allowed to mutate state within the update
method of the store. Subscribers to the store will only see the updated state after the function exits. Karma uses immer under the hood, so the same rules apply (state must consist of plain objects and arrays without circular references).
If you use multiple stores in your application, make sure to get the correct one from a React component:
function AddTodoButton(props: { todo: ITodo }) {
const store = useStore()
function onPress() {
store.update((s) => {
s.todos.push(props.todo)
})
}
return <button onPress={onPress}>Add</button>
}
A better way might be to use the useStoreUpdate
hook, which will automatically wrap the update function in useCallback
, which can avoid re-renders when passed down to child components:
function AddTodoButton(props: { todo: ITodo }) {
const onPress = useStoreUpdate(
// Updates done to the store when onPress is called
(s) => {
s.todos.push(props.todo)
},
// DependencyList passed to useCallback
//all variables used in the update function should be put here
[props.todo]
)
return <button onPress={onPress}>Add</button>
}
Dealing with asynchronicity is often an issue with Redux-like solutions. Usually, you have to think about whether to use Thunks or Sagas or something completely different.
With Karma, there is just the update
function, which may never be async. Don't overthink it, just update the state synchronously whenever something relevant happens:
async function downloadMultipleFiles(urls: string[]) {
// Set the loading state
store.update((s) => {
s.downloadInProgress = true
})
// Download files in parallel
await Promise.all(urls.map(downloadSingleFile))
// All downloading done
store.update((s) => {
s.downloadInProgress = false
})
}
async function downloadSingleFile(url: string) {
const data = await fetch(url)
await storeFileOnDisk(url, data)
// Update the state after each file done
store.update((s) => {
s.filesDownloaded.push(url)
})
}
Should work out of the box. Note that since our "actions" are just anonymous lambda functions, all of them will be called "Anonymous Action".