Loflux
A sort-of-Flux-like state management library for React for creating stores that are:
-
✅ Flux-like (unidirectional data flow). -
✅ Hook-based. -
✅ Immutable via Immer. -
✅ Fully type-safe. -
✅ Not wrapped around your whole app. -
✅ Render-efficient.
Check out a live demo here! Interested in the classic TODO example? Here you go!. For a more dynamic example, check out this live demo.
Basic Example
Let's get you up and running with a simple example that illustrates how to use loflux
.
Install the library
Install from NPM:
# Using NPM
npm install loflux
# Or using Yarn
yarn add loflux
Create a Store
Start by creating a store. Your store will be type-safe based on the initial state and actions.
import { createStore } from "loflux";
const { useData, dispatch, useActionResponse } = createStore({
initialState: { name: "Jane Doe", age: 32 },
actions: {
changeName: (draft, name: string) => {
draft.name = name;
},
changeAge: (draft, age: number) => {
draft.age = age;
},
requestDestroy: () => {
}
}
});
Use Data from Your Store
Now, let's subscribe to some data that's in the store!
const { useData } = createStore({ ... });
const NameDisplay = () => {
const name = useData((s) => s.name);
return <h1>Hello, {name}!</h1>;
}
The argument to the useData
hook here is a selector function used to pull out the data that you want. This component only
re-renders when the name
value changes!
Change Data in Your Store
Chances are, if you're using some sort of store, your data needs to change over time. Let's check that out.
const { useData, dispatch, ... } = createStore({ ... });
const NameInput = () => {
const name = useData((s) => s.name);
return (
<input
value={name}
onChange={(e) => dispatch('changeName', e.target.value)}
/>
);
}
The 'changeName'
argument might look like a magic-string, but it's actually type-safe and based on the actions that
you provide. Changing that to 'foobar'
is going to really upset TypeScript.
Furthermore, the payload of that dispatched action is type-safe, too! Don't try to pass a number
as the second argument if the action you're trying to dispatch isn't expecting it! In general, the last parameters of your actions are the last arguments you should pass through to the action when it's dispatched!
Subscribe to Actions
Sometimes you want to just respond to actions that pass through your store (e.g., pressing a button in one component triggers some sort of action in some other component that's far away).
const { dispatch, useActionResponse } = createStore({ ... });
const DestructionButton = () => {
return <button onClick={() => dispatch('requestDestroy')}>Try to destory</button>;
}
const OverlayModal = () => {
const [showConf, setShowConf] = React.useState(false);
useActionResponse('requestDestroy', () => {
setShowConf(true);
});
return showConf ? <div>Confirmation modal...</div> : null;
}
API
createStore
function
The The createStore
function creates a store with a given state shape and set of actions that can be dispatched, and returns the store object (you likely don't need this) and some utility functions to access the store data and dispatch actions. The
signature is:
createStore = (options: { initialState, actions }) => ({ store, useData, dispatch, useActionResponse });
with the following options:
Options | Type | Required? | Description |
---|---|---|---|
initialState |
S extends any |
Initial state for store. The type of this will type the rest of your store. | |
actions |
{ [key: string]: (draft: Draft<S>, payload: any) => void; } |
Map of actions that can be dispatched, and how they affect the store's state. Uses Immer.js, so draft can be acted on as if it was mutable. |
Here's a quick example:
import { createStore } from "loflux";
const { store, useData, dispatch, useActionResponse } = createStore({
initialState: { name: "Jane Doe", age: 32 },
actions: {
changeName: (draft, name: string) => {
draft.name = name;
},
changeAge: (draft, age: number) => {
draft.age = age;
},
requestDestroy: () => {}
}
});
createStore().useData
hook
The The createStore
function will return a useData
hook that you can use to access data. The signature for this hook is:
createStore().useData = (selector: (state: State) => any) => ReturnType<typeof selector>;
Use this selector to pluck out the data that you need, or do any massaging you need. For advanced computed/derived values, consider using this in conjunction with React.useMemo
.
createStore().dispatch
method
The The createStore
function will return a dispatch
method that you can use to dispatch actions to the store. The signature for this hook is:
createStore().dispatch = (actionName, ...payload) => void;
You pass in an action name as a string (corresponds to the actions you declared when creating the store), and payload value(s) that matches the type of the payload parameter(s) of the declared action.
Here's an example of using more than one payload argument:
const { dispatch } = createStore({
initialState: { name: 'Grant', age: 28 },
actions: {
changeNameAndAge: (draft, name: string, age: number) => {
draft.name = name;
draft.age = age;
}
}
});
// ... later on
dispatch('changeNameAndAge', 'John Doe', 37);
createStore().useActionResponse
hook
The The createStore
function returns a useActionResponse
hook that responds to specified actions that pass through the store. You indicate which action type(s) you want to listen for, and a callback to run when those actions occur.
useStoreData
hook
The You probably don't need this hook, createStore().useData
is an easy-access pattern for this. The useStoreData
hook is a way to extract/subscribe to store data from a particular store. You provide a store and a selector that selects that data in the store that you want to subscribe to, and the hook gives you back the selected data. Updates to that data trigger re-renders. The signature is:
useStoreData(store: Store, selector: (state: State) => any);
Here's a quick example:
const { store: profileStore } = createStore({
initialState: { name: "Jane Doe", age: 32 },
actions: { ... }
});
const MyComponent = () => {
const name = useStoreDate(profileStore, s => s.name);
return <div>Hello, {name}!</div>;
}
useActionDispatcher
hook
The You probably don't need this hook, createStore().dispatch
is an easy-access pattern for this. The useActionDispatcher
hook is a way to dispatch actions to a specified store. You provide a store and the name of an action (from you action list provided in createStore
), and the hook will return a function you can call to dispatch the specified action. The signature is:
useActionDispatcher(store: Store, actionName: string);
Here's a quick example:
const { store: profileStore } = createStore({
initialState: {name: "Jane Doe", age: 32},
actions: {
updateName: (draft, newName: string) => {
draft.name = newName;
}
}
});
const MyComponent = () => {
const updateName = useActionDispatcher(profileStore, "updateName");
return <button onClick={() => updateName("Susan!")}>Change to Susan!</button>;
}
useActionEffect
hook
The You probably don't need this hook, createStore().useActionResponse
is an easy-access pattern for this. The useActionEffect
hook is a way to respond to actions that get dispatched. The signature is:
useActionEffect(store: Store, actionName: string | string[], effect: (newState: State) => void);
Here's a quick example:
const { store: profileStore } = createStore({
initialState: {name: "Jane Doe", age: 32},
actions: {
updateName: (draft, newName: string) => {
draft.name = newName;
}
}
});
const MyComponent = () => {
useActionEffect(profileStore, "updateName", () => {
console.log("updateName was fired! Do something cool.");
});
return <button onClick={() => updateName("Susan!")}>Change to Susan!</button>;
}
TODO:
- [x] Actions can take more than one argument, spread thru args
- [ ] Thunk support