- Notifications
You must be signed in to change notification settings - Fork107
Simple React state management. Made with ❤️ and ES6 Proxies.
License
RisingStack/react-easy-state
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
React Easy State - byRisingStack
NEWS: v6.3.0 fixed a nasty bug that could renderzombie children. Please update to this version at least to save yourself some headaches. Thanks!
Table of Contents
React Easy State is a practical state management library with two functions and two accompanying rules.
- Always wrap your components with
view(). - Always wrap your state store objects with
store().
importReactfrom'react';import{store,view}from'@risingstack/react-easy-state';constcounter=store({num:0,increment:()=>counter.num++});exportdefaultview(()=>(<buttononClick={counter.increment}>{counter.num}</button>));
This is enough for it to automatically update your views when needed. It doesn't matter how you structure or mutate your state stores, any syntactically valid code works.
Check thisTodoMVC codesandbox orraw code for a more exciting example with nested data, arrays and getter properties.
npm install @risingstack/react-easy-state
Setting up a quick project
Easy State supportsCreate React App without additional configuration. Just run the following commands to get started.
npx create-react-app my-appcd my-appnpm install @risingstack/react-easy-statenpm startYou need npm 5.2+ to use npx.
store creates a state store from the passed object and returns it. A state store behaves just like the passed object. (To be precise, it is a transparent reactive proxy of the original object.)
import{store}from'@risingstack/react-easy-state';constuser=store({name:'Rick'});// stores behave like normal JS objectsuser.name='Bob';
State stores may have arbitrary structure and they may be mutated in any syntactically valid way.
import{store}from'@risingstack/react-easy-state';// stores can include any valid JS structure// including nested data, arrays, Maps, Sets, getters, setters, inheritance, ...constuser=store({profile:{firstName:'Bob',lastName:'Smith',getname(){return`${user.profile.firstName}${user.profile.lastName}`;},},hobbies:['programming','sports'],friends:newMap(),});// stores may be mutated in any syntactically valid wayuser.profile.firstName='Bob';deleteuser.profile.lastName;user.hobbies.push('reading');user.friends.set('id',otherUser);
Async operations can be expressed with the standard async/await syntax.
import{store}from'@risingstack/react-easy-state';constuserStore=store({user:{},asyncfetchUser(){userStore.user=awaitfetch('/user');},});exportdefaultuserStore;
State stores may import and use other state stores in their methods.
userStore.js
import{store}from'@risingstack/react-easy-state';constuserStore=store({user:{},asyncfetchUser(){userStore.user=awaitfetch('/user');},});exportdefaultuserStore;
recipesStore.js
import{store}from'@risingstack/react-easy-state';importuserStorefrom'./userStore';constrecipesStore=store({recipes:[],asyncfetchRecipes(){recipesStore.recipes=awaitfetch(`/recipes?user=${userStore.user.id}`,);},});exportdefaultrecipesStore;
Wrap your state stores withstore as early as possible.
// DON'T DO THISconstperson={name:'Bob'};person.name='Ann';exportdefaultstore(person);
// DO THIS INSTEADconstperson=store({name:'Bob'});person.name='Ann';exportdefaultperson;
The first example wouldn't trigger re-renders on theperson.name = 'Ann' mutation, because it is targeted at the raw object. Mutating the raw - nonestore-wrapped object - won't schedule renders.
Avoid using thethis keyword in the methods of your state stores.
import{store,view}from'@risingstack/react-easy-state';constcounter=store({num:0,increment(){// DON'T DO THISthis.num++;// DO THIS INSTEADcounter.num++;},});exportdefaultview(()=>(<divonClick={counter.increment}>{counter.num}</div>));
this.num++ won't work, becauseincrement is passed as a callback and loses itsthis. You should use the direct object reference -counter - instead ofthis.
Wrapping your components withview turns them into reactive views. A reactive view re-renders whenever a piece of store - used inside its render - changes.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';// this is a global state storeconstuser=store({name:'Bob'});// this is re-rendered whenever user.name changesexportdefaultview(()=>(<div><inputvalue={user.name}onChange={ev=>(user.name=ev.target.value)}/><div>Hello{user.name}!</div></div>));
Wrap ALL of your components withview - including class and function ones - even if they don't seem to directly use a store.
Every component that is using a store or part of a store inside its render must be wrapped withview. Sometimes store usage is not so explicit and easy to to miss.
import{view,store}from'@risingstack/react-easy-state';constappStore=store({user:{name:'Ann'},});constApp=view(()=>(<div><h1>My App</h1><Profileuser={appStore.user}/></div>));// DO THISconstProfile=view(({ user})=><p>Name:{user.name}</p>);// DON'T DO THIS// This won't re-render on appStore.user.name = 'newName' like mutationsconstProfile=({ user})=><p>Name:{user.name}</p>;
If you are100% sure that your component is not using any stores you can skip theview wrapper.
importReactfrom'react';// you don't have to wrap this component with `view`exportdefault(()=><p>This is just plain text</p>);
view wrapping is advised even in these cases though.
- It saves you from future headaches as your project grows and you start to use stores inside these components.
viewis pretty much equivalent tomemoif you don't use any stores. That is nearly always nice to have.
A single reactive component may use multiple stores inside its render.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';constuser=store({name:'Bob'});consttimeline=store({posts:['react-easy-state']});// this is re-rendered whenever user.name or timeline.posts[0] changesexportdefaultview(()=>(<div><div>Hello{user.name}!</div><div>Your first post is:{timeline.posts[0]}</div></div>));
view implements an optimalshouldComponentUpdate (ormemo) for your components.
Using
PureComponentormemowill provide no additional performance benefits.Defining a custom
shouldComponentUpdatemay rarely provide performance benefits when you apply some use case specific heuristics inside it.
Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';constuser=store({name:'Bob',age:30});functionmutateUser(){user.name='Ann';user.age=32;}// calling `mutateUser` will only trigger a single re-render of the below component// even though it mutates the store two times in quick successionexportdefaultview(()=>(<divonClick={mutateUser}> name:{user.name}, age:{user.age}</div>));
If you mutate your stores multiple times synchronously fromexotic task sources, multiple renders may rarely happen. If you experience performance issues you can batch changes manually with thebatch function.batch(fn) executes the passed function immediately and batches any subsequent re-renders until the function execution finishes.
importReactfrom'react';import{view,store,batch}from'@risingstack/react-easy-state';constuser=store({name:'Bob',age:30});functionmutateUser(){// this makes sure the state changes will cause maximum one re-render,// no matter where this function is getting invoked frombatch(()=>{user.name='Ann';user.age=32;});}exportdefaultview(()=>(<div> name:{user.name}, age:{user.age}</div>));
NOTE: The React team plans to improve render batching in the future. The
batchfunction and built-in batching may be deprecated and removed in the future in favor of React's own batching.
Always applyview as the latest (innermost) wrapper when you combine it with other Higher Order Components.
import{view}from'@risingstack/react-easy-state';import{withRouter}from'react-router-dom';import{withTheme}from'styled-components';constComp=()=><div>A reactive component</div>;// DO THISwithRouter(view(Comp));withTheme(view(Comp));// DON'T DO THISview(withRouter(Comp));view(withTheme(Comp));
Usage with (pre v4.4) React Router.
If routing is not updated properly, wrap your
view(Comp)- with theRoutes inside - inwithRouter(view(Comp)). This lets react-router know when to update.The order of the HOCs matter, always use
withRouter(view(Comp)).
This is not necessary if you use React Router 4.4+. You can find more details and some reasoning about this inthis react-router docs page.
Usage with React Developer Tools.
If you want React Developer Tools to recognize your reactive view components' names, you have to pass either anamed function or an anonymous function withname inference to theview wrapper.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';constuser=store({name:'Rick',});constcomponentName=()=>(<div>{user.name}</div>);exportdefaultview(componentName);
Passing nested data to third party components.
Third party helpers - like data grids - may consist of many internal components which can not be wrapped byview, but sometimes you would like them to re-render when the passed data mutates. Traditional React components re-render when their props change by reference, so mutating the passed reactive data won't work in these cases. You can solve this issue by deep cloning the observable data before passing it to the component. This creates a new reference for the consuming component on every store mutation.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';importTablefrom'rc-table';importcloneDeepfrom'lodash/cloneDeep';constdataStore=store({items:[{product:'Car',value:12,},],});exportdefaultview(()=>(<Tabledata={cloneDeep(dataStore.items)}/>));
A singleton global store is perfect for something like the current user, but sometimes having local component states is a better fit. Just create a store inside a function component or as a class component property in these cases.
importReactfrom'react'import{view,store}from'@risingstack/react-easy-state'exportdefaultview(()=>{constcounter=store({num:0})constincrement=()=>counter.num++return<button={increment}>{counter.num}</button>})
Local stores in functions rely on React hooks. They require React and React DOM v16.8+ or React Native v0.59+ to work.
You can use React hooks - includinguseState - in function components, Easy State won't interfere with them. Consider usingautoEffect instead of theuseEffect hook for the best experience though.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';exportdefaultview(()=>{const[name,setName]=useState('Ann');constuser=store({age:30});return(<div><inputvalue={name}onChange={ev=>setName(ev.target.value)}/><inputvalue={user.age}onChange={ev=>(user.age=ev.target.value)}/></div>);});
importReact,{Component}from'react';import{view,store}from'@risingstack/react-easy-state';classCounterextendsComponent{counter=store({num:0});increment=()=>counter.num++;render(){return(<buttononClick={this.increment}>{this.counter.num}</button>);}}exportdefaultview(Counter);
You can also use vanillasetState in your class components, Easy State won't interfere with it.
importReact,{Component}from'react';import{view,store}from'@risingstack/react-easy-state';classProfileextendsComponent{state={name:'Ann'};user=store({age:30});setName=ev=>this.setState({name:ev.target.value});setAge=ev=>(this.user.age=ev.target.value);render(){return(<div><inputvalue={this.state.name}onChange={this.setName}/><inputvalue={this.user.age}onChange={this.setAge}/></div>);}}exportdefaultview(Profile);
Don't name local stores asstate. It may conflict with linter rules, which guard against direct state mutations.
importReact,{Component}from'react';import{view,store}from'@risingstack/react-easy-state';classProfileextendsComponent{// DON'T DO THISstate=store({});// DO THISuser=store({});render(){}}
Deriving local stores from props (getDerivedStateFromProps).
Class components wrapped withview have an extra staticderiveStoresFromProps lifecycle method, which works similarly to the vanillagetDerivedStateFromProps.
importReact,{Component}from'react';import{view,store}from'@risingstack/react-easy-state';classNameCardextendsComponent{userStore=store({name:'Bob'});staticderiveStoresFromProps(props,userStore){userStore.name=props.name||userStore.name;}render(){return<div>{this.userStore.name}</div>;}}exportdefaultview(NameCard);
Instead of returning an object, you should directly mutate the received stores. If you have multiple local stores on a single component, they are all passed as arguments - in their definition order - after the first props argument.
UseautoEffect to react with automatic side effect to your store changes. Auto effects should contain end-of-chain logic - like changing the document title or saving data to LocalStorage.view is a special auto effect that does rendering.
Never use auto effects to derive data from other data. Use dynamic getters instead.
import{store,autoEffect}from'@risingstack/react-easy-state';// DON'T DO THISconststore1=store({name:'Store 1'})conststore2=store({name:'Store 2'})autoEffect(()=>store2.name=store1.name)// DO THIS INSTEADconststore1=store({name:'Store 1'})conststore2=store({getname(){returnstore1.name}})
Global auto effects can be created withautoEffect and cleared up withclearEffect.
import{store,autoEffect,clearEffect}from'@risingstack/react-easy-state';constapp=store({name:'My App'})consteffect=autoEffect(()=>document.title=app.name)// this also updates the document titleapp.name='My Awesome App'clearEffect(effect)// this won't update the document title, the effect is clearedapp.name='My App'
Use local auto effects in function components instead of theuseEffect hook when reactive stores are used inside them. These local effects are automatically cleared when the component unmounts.
importReactfrom'react'import{store,view,autoEffect}from'@risingstack/react-easy-state';exportdefaultview(()=>{constapp=store({name:'My App'})// no need to clear the effectautoEffect(()=>document.title=app.name)})
Explicitly pass none reactive dependencies - like vanillas props and state - to local auto effects in function components.
Because of the design of React hooks you have to explicitly pass all none reactive data to a hook-like dependency array. This makes sure that the effect also runs when the none reactive data changes.
importReactfrom'react'import{store,view,autoEffect}from'@risingstack/react-easy-state';exportdefaultview(({ greeting})=>{constapp=store({name:'My App'})// pass `greeting` in the dependency array because it is not coming from a storeautoEffect(()=>document.title=`${greeting}${app.name}`,[greeting])})
Local effects in class components must be cleared when the component unmounts.
importReact,{Component}from'react'import{store,view,autoEffect}from'@risingstack/react-easy-state';classAppextendsComponent{app=store({name:'My App'})componentDidMount(){this.effect=autoEffect(()=>document.title=this.app.name)}componentWillUnmount(){// local effects in class components must be cleared on unmountclearEffect(this.effect)}}
Creates an observable store from the passed object and returns it. Can be used outside components forglobal stores and inside components forlocal stores.
import{store}from'@risingstack/react-easy-state';constuser=store({name:'Rick'});
Creates areactive view from the passed component and returns it. A reactive view re-renders whenever any store data used inside it is mutated.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';constuser=store({name:'Bob'});exportdefaultview(()=>(<div>Hello{user.name}!</div>));
Immediately executes the passed function and batches all store mutations inside it. Batched mutations are guaranteed to not trigger unnecessary double renders. Most task sources are batched automatically, only usebatch if you encounter performance issues.
importReactfrom'react';import{view,store}from'@risingstack/react-easy-state';constuser=store({name:'Bob'});functionsetName(){batch(()=>{user.name='Rick'user.name='Ann'})}
Creates a reactive function from the passed one, immediately executes it, and returns it. A reactive function automatically re-reruns whenever any store data used inside it is mutated.
Can be used bothoutside andinside components.
import{store,autoEffect}from'@risingstack/react-easy-state';constuser=store({name:'Bob'})autoEffect(()=>document.title=user.name)
Takes a reactive function (returned byautoEffect) and clears the reactivity from it. Cleared reactive functions will no longer re-rerun on related store mutations. Reactive functions created inside function components are automatically cleared when the component unmounts.
import{store,autoEffect,clearEffect}from'@risingstack/react-easy-state';constuser=store({name:'Bob'})consteffect=autoEffect(()=>document.title=user.name)clearEffect(effect)
- Clock Widget (source) (codesandbox) (react-native source) (react-native sandbox): a reusable clock widget with a tiny local state store.
- Stopwatch (source) (codesandbox) (tutorial): a stopwatch with a mix of normal and getter state properties.
- Pokédex (source) (codesandbox): a Pokédex app build with Apollo GraphQL, async actions and a global state.
- TodoMVC (source) (codesandbox): a classic TodoMVC implementation with a lot of getters/setters and implicit reactivity.
- Contacts Table (source) (codesandbox): a data grid implementation with a mix of global and local state.
- Beer Finder (source) (codesandbox) (tutorial): an app with async actions and a mix of local and global state, which finds matching beers for your meal.
- Introducing React Easy State: making a simple stopwatch.
- Stress Testing React Easy State: demonstrating Easy State's reactivity with increasingly exotic state mutations.
- Design Patterns with React Easy State: demonstrating async actions and local and global state management through a beer finder app.
- The Ideas Behind React Easy State: a deep dive under the hood of Easy State.
You can compare Easy State with plain React and other state management libraries with the below benchmarks. It performs a bit better than MobX and similarly to Redux.
- Node: 6 and above
- Chrome: 49 and above
- Firefox: 38 and above
- Safari: 10 and above
- Edge: 12 and above
- Opera: 36 and above
- React Native: 0.59 and above
This library is based on non polyfillable ES6 Proxies. Because of this, it will never support IE.
This library detects if you use ES6 or commonJS modules and serve the right format to you. The default bundles use ES6 features, which may not yet be supported by some minifier tools. If you experience issues during the build process, you can switch to one of the ES5 builds from below.
@risingstack/react-easy-state/dist/es.es6.jsexposes an ES6 build with ES6 modules.@risingstack/react-easy-state/dist/es.es5.jsexposes an ES5 build with ES6 modules.@risingstack/react-easy-state/dist/cjs.es6.jsexposes an ES6 build with commonJS modules.@risingstack/react-easy-state/dist/cjs.es5.jsexposes an ES5 build with commonJS modules.
If you use a bundler, set up an alias for@risingstack/react-easy-state to point to your desired build. You can learn how to do it with webpackhere and with rolluphere.
Contributions are always welcome, please read ourcontributing documentation.
Thanks goes to these wonderful people (emoji key):
Miklos Bertalan 💻 | Roland 💻 | Daniel Gergely 💻🎨💡 | Peter Czibik 🚇 |
This project follows theall-contributors specification. Contributions of any kind welcome!
About
Simple React state management. Made with ❤️ and ES6 Proxies.
Topics
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors14
Uh oh!
There was an error while loading.Please reload this page.
