defi-react
A super straightforward state management library for React in 5 hooks and 2 methods. Get rid of useless component re-renders!
Quick start
Install deps.
npm i defi defi-react
# or
yarn add defi defi-react
Create store and wrap your app by DefiProvider
(it's not a requirement, but a recommendation, see docs below).
;;// store can be literally any object// but for structural purposes it's recommended to create a class// see examples belowconst store = foo: 1 ; <DefiProvider value=store> <App /> </DefiProvider>
Make defi-react do its job!
; const App = { const foo setFoo = ; // listen for store.foo changes return <div> Foo:' 'foo <button onClick= >Change "foo" to 42</button> </div> } ;
Jump to examples to see how you can define store class
Table of contents
Why?
Being many years a React developer I've found out that app-wide state management in React is tricky. I've got an idea to create my own state management solution after I started to work on my own React Native project and tried to find out what I'd like use as an app state library. In my regular work the main tool for this task usually was Redux but with my own project I'm not tied by market standards and decided to develop something super simple for my needs. I was tired by all these actions, reducers, constants, action creators, sagas, middlewares, but couldn't find any simple and flexible alternative for Redux. To be fair enough there is a list of alternatives I should mention.
Alternatives
- The first is Redux. This is s amazing library in terms of how many users know it. My favorite part of Redux (to be precise react-redux) is
useSelector
hook which allows to get only what you want and receive components updates only if that you need is updated. But as I noted above it's overcomplicated in most of cases in my opinion. This issue can be partially solved by rematch which is definitely a recommended library if you're going to use Redux. - MobX is a cool library. Unfortunately it's cool only in case if you use class components but in the new world of hooks it appears to be not that elegant because you have to wrap returned React elements by useObserver. I don't say it's bad but I really don't like the idea to return something else than regular React elements from pure React components. In other words, that just my personal preference and if you OK with that, use MobX as a cool, battle-tested and second library by popularity after Redux.
- Apollo Client is also a highlighted library. It allows to bring Graphql syntax to your local store which is super cool when you also have a server powered by Graphql. In case if you don't use Graphql on server-side I think it appears to be too complicated to solve state management problem.
- WatermelonDB (React Native only) provides a nice ORM powered by SQLite to store your local data. In my specific case I had no need to store data locally except of what needs to be sent to Firebase DB. Since Firebase supports offline mode and stores offline data by itself I keep WatermelonDB for cases where I really need such a great and powerful tool.
Pros & cons
Pros
- Clear and simple API.
- Minimum work to get started (no actions, middlewares, etc.).
- Minimum concepts to get started (no such things as observables, decorators, etc.), just built-in setters (for listenable properties) and custom events.
- Render components only when used peace of data is updated.
- Use any object as global store or as component's own store (or multiple stores if needed).
Cons
- Both defi and defi-react aren't that popular as other well-known libraries.
- Less structural requirements (you should architect your store by your own).
What do I need to know about defi.js?
defi is a library which enhances JavaScript objects with Object.defineProperty
. By defining accessors it turns any object into an event target and also allows to subscribe to property change events.
To make you easier to start using defi-react hooks there is a quick reference to a few methods you're going to need while you implement your store.
defi.on
on(target: object, eventName: string | string[], callback: (...triggerArgs: any[]) => void)
The function makes the target object to become an event target. A special event name "change:KEY"
allows to listen properties re-definition. Events can be triggered by defi.trigger
described next.
defi.trigger
trigger(target: object, eventName, callback)
Triggers custom events.
; const object = {}; ;; ; // logs "customEvent is triggered with args [1, 2, 3]"objectx = 'foo'; // logs x is changed to foo
Also it's worthy to mention some other useful defi.js methods in case if you want to deepen into defi.js topic: set (sets properties with some special flags like silent: true
), off (removes event listener; the hooks do it automatically when needed), calc (defines calculated properties), mediate (controls type of object properties) and finally chain (allows to chain method calls).
defi also includes some DOM manipulation methods (such as bindNode
, bound
etc), but at this case you don't need them at all because rendering is 100% handled by React.
Reference
All hooks accept an object
or storeSelector
function as first argument (the only exception is useStore
which doesn't accept object type). object
type argument allows to handle any object (even if it isn't a part of app store) storeSelector
in its turn is a function which is going to return some object from the store (at this case store is going to be taken from defi react context).
Context and Provider
As a widely used practice in the React ecosystem is the concept of context providers. You can wrap your app with Provider
component and pass your store as value
arg to make the store to be accessible everywhere within application you make. You can skip this but you'll be required to pass store object to hooks every time. You may want to skip it in case if you're going to try defi-react on existing application components.
;// store can be literally any object// but for structural purposes it's recommended to create a class// see examples belowconst store = {}; ... <DefiProvider value=store> ... </DefiProvider>
If needed, you can access store context which is also exported by the library:
;...// call it inside some of child componentsconst store = ;
useStore
useStore(storeSelector?) => object
This is simple but at the same time an important hook. The only thing it does is it returns store context value. In other words it does the same as useContext(DefiContext)
. You can also pass a selector function which should return store slice in case if you need to get access to a nested object.
const store = ; // returns storeconst foo = ; // extracts foo from the storeconst foo = ; // 100% equivalent to the previous line
An important thing to know is that useStore
hook never updates components where it's used. It's puepose is to get access to your store object but not to make components to react on changes. You should use useChange
hook for that.
useChange
useChange(storeSlice: object | storeSelector, key: string) => [value: any, updateValue: function]
Listens to object property changes and re-renders components when the change appears. A cool thing about this hook is the fact that components are going to listen to needed changes only and be silent when something else is changed in the store or custom listened object.
const foo setFoo = ; // listen for store.foo changesconsole; // logs current storeObject.foo value; // sets storeObject.foo value to 'bar' // ... or somewhere in your code ...storeObjectfoo = 'bar'; // this will re-render components where storeObject.foo is listened by useChange
const foo setFoo = ; // listen for store.baz.foo changes
const foo setFoo = ; // listen for someObject.foo changes
useSet
useSet(storeSlice: object | storeSelector, key: string) => set
Returns update function for a given object peoperty. It's goal is to make possible to update a property without re-render of a component where the hook is used. All components which do use useChange
with the given property are going to be re-rendered if value is changed.
const store = ;const setFoo = ; ; // which is equivalent to storefoo = 42;
const setBar = ; ; // which is equivalent to storefoobar = 42;
useOn
useOn(storeSlice: object | storeSelector, eventName) => trigger
Subscribes component to a given event on an object (in other words re-renders when the event is triggered). Returns trigger function to make possible to fire the event in component body.
const triggerBar = ; ...<button onClick=triggerBar></button>
// somewhere outside of the component... ; // triggers bar and notifies all components which listen that event with useOn
useTrigger
useTrigger(storeSlice: object | storeSelector, eventName) => trigger
The hook is very similar to useOn
but it doesn't re-render component when an event is triggered. the only thing it does is it returns trigger function to fire events which may be listened by store or by other compoents which use useOn
.
const triggerBar = ; ...<button onClick=triggerBar></button>
Examples
Store class
As it mentioned above you can use any object as store but it's recommended to use classes to keep your store well structured. Let's say store has auth
"sub-store" which is created by Auth
class.
// ----- store.js -----; email = ''; password = ''; token = null; error = null; { try const email password = this; const token = await ; thistoken = token; catche thiserror = 'Unable to authenticate'; } { thisauth = ; // listen changes at this.token and this.error ; } ; // ----- App.js -----;; <DefiProvider value=store> <Authentication /> </DefiProvider> // ----- Authentication.js ----- { const auth = ; const email setEmail = ; const password setPassword = ; const token = ; const error = ; return <form> Token:' 'token <br /> Error:' 'error /* ... email and password inputs are here ... */ <button onClick=authauthenticate>Authenticate</button> </form> }
Array rendering and its modification
At this example we define an array which is going to be rendered by a component. Data modifications is very similar to what you may do with Redux. To make components re-render because of some peace of data is changed you need to reassign store slice property. In other words if you run useChange(storeSlice, someArrayField)
then storeSlice[someArrayField]
needs to be re-asssigned instead of doing storeSlice[someArrayField].push(...)
.
As you may notice there is no such thing as "action". Modifications (especially complex), side-effects, and any other things are recommended to be defined as class methods.
If you want more examples (like if you want to see how deletion needs to be implemented) feel free to create an issue. But everything with defi-react should be quite straightforward as it mentioned in the library description.
// ----- store.js ----- // that's the array you want to render items = ; { // add items by re-assign, not by modification thisitems = ...thisitems item; } { // update items also by re-assignx (Array#map returns a new array) thisitems = thisitems } // ----- Items.js-----; { const store = ; // we don't use update function here since it's handled by addItem, updateItem methods const items = ; // get these store methods const addItem updateItem = store; return <div> items <button onClick= >Add item</button> </div> } // ----- Item.js ----- { return <div> Foo:' 'foo <button onClick= >Update "foo" to random</button> </div> ;}