z-preact-easy-state
v6.0.12
Published
Forked react state management with a minimal API. Made with ES6 Proxies. Adopted for preact
Downloads
8
Maintainers
Readme
Preact Easy State
This project was forked from react-easy-state. preact-easy-state in npm was not up to date, and had not a github repo. I decided to do things right. Also this repo contains correct TypeScript header.
Simple Preact state management. Made with :heart: and ES6 Proxies.
Breaking change in v6: the default bundle changed from the ES5 version to the ES6 version. If you experience problems during the build process, please check this docs section.
- Original documentation
Introduction
Easy State is a transparent reactive state management library with two functions and two accompanying rules.
- Always wrap your components with
view()
. - Always wrap your state store objects with
store()
.
import {h, Component} from 'preact'
import { store, view } from 'z-preact-easy-state'
const counter = store({
num: 0,
incr: () => counter.num++
})
export default view(() => <button onClick={counter.incr}>{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 this TodoMVC codesandbox or raw code for a more exciting example with nested data, arrays and computed values.
Installation
npm install react-easy-state
Easy State supports Create React App without additional configuration. Just run the following commands to get started.
npx create-react-app my-app
cd my-app
npm install react-easy-state
npm start
You need npm 5.2+ to use npx.
Usage
Creating stores
store
creates a state store from the passed object and returns it. State stores are just like normal JS objects. (To be precise, they are transparent reactive proxies of the original object.)
import { store } from 'z-preact-easy-state'
const user = store({
name: 'Rick'
})
// stores behave like normal JS objects
user.name = 'Bob'
import { store } from 'z-preact-easy-state'
// stores can include any valid JS structure (nested data, arrays, getters, Sets, ...)
const user = store({
profile: {
firstName: 'Bob',
lastName: 'Smith',
get name () {
return `${user.firstName} ${user.lastName}`
}
}
hobbies: ['programming', 'sports']
})
// stores can be mutated in any syntactically valid way
user.profile.firstName = 'Bob'
delete user.profile.lastName
user.hobbies.push('reading')
Creating reactive views
Wrapping your components with view
turns them into reactive views. A reactive view re-renders whenever a piece of store - used inside its render - changes.
import { h, Component } from 'preact'
import { view, store } from 'z-preact-easy-state'
const user = store({ name: 'Bob' })
class HelloComp extends Component {
onChange = ev => (user.name = ev.target.value)
// the render is triggered whenever user.name changes
render() {
return (
<div>
<input value={user.name} onChange={this.onChange} />
<div>Hello {user.name}!</div>
</div>
)
}
}
// the component must be wrapped with `view`
export default view(HelloComp)
import { h, Component } from 'preact'
import { view, store } from 'z-preact-easy-state'
const user = store({ name: 'Bob' })
const timeline = store({ posts: ['react-easy-state'] })
class App extends Component {
onChange = ev => (user.name = ev.target.value)
// render is triggered whenever user.name or timeline.posts[0] changes
render() {
return (
<div>
<div>Hello {user.name}!</div>
<div>Your first post is: {timeline.posts[0]}</div>
</div>
)
}
}
// the component must be wrapped with `view`
export default view(App)
Make sure to wrap all of your components with view
- including stateful and stateless ones. If you do not wrap a component, it will not properly render on store mutations.
Creating local stores
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 as a component property in these cases.
import { h, Component } from 'preact'
import { view, store } from 'z-preact-easy-state'
class ClockComp extends Component {
clock = store({ time: new Date() })
componentDidMount() {
setInterval(() => (this.clock.time = new Date()), 1000)
}
render() {
return <div>{this.clock.time}</div>
}
}
export default view(ClockComp)
Original documentation
That's it, You know everything to master React state management! Check some of the examples and articles for more inspiration or the FAQ section for common issues.
Examples with live demos
Beginner
- Clock Widget (source) (codesandbox): a reusable clock widget with a tiny local state store.
- Stopwatch (source) (codesandbox) (tutorial): a stopwatch with a mix of normal and computed state properties.
Advanced
- TodoMVC (source) (codesandbox): a classic TodoMVC implementation with a lot of computed data 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.
Articles
- 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.
FAQ and Gotchas
What triggers a re-render?
Easy State monitors which store properties are used inside each component's render method. If a store property changes, the relevant renders are automatically triggered. You can do anything with stores without worrying about edge cases. Use nested properties, computed properties with getters/setters, dynamic properties, arrays, ES6 collections and prototypal inheritance - as a few examples. Easy State will monitor all state mutations and trigger renders when needed. (Big cheer for ES6 Proxies!)
My store methods are broken
You should avoid using the this
keyword in the methods of your state stores.
const counter = store({
num: 0,
increment() {
this.num++
}
})
export default view(() => <div onClick={counter.increment}>{counter.num}</div>)
The above snippet won't work, because increment
is passed as a callback and loses its this
. You should use the direct object reference - counter
in this case - instead of this
.
const counter = store({
num: 0,
increment() {
counter.num++
}
})
This works as expected, even when you pass store methods as callbacks.
My views are not rendering
You should wrap your state stores with store
as early as possible to make them reactive.
const person = { name: 'Bob' }
person.name = 'Ann'
export default store(person)
The above example wouldn't trigger re-renders on the person.name = 'Ann'
mutation, because it is targeted at the raw object. Mutating the raw - none store
wrapped object - won't schedule renders.
Do this instead of the above code.
const person = store({ name: 'Bob' })
person.name = 'Ann'
export default person
My views render multiple times unnecessarily
Re-renders are batched 99% percent of the time until the end of the state changing function. You can mutate your state stores multiple times in event handlers, async functions and timer and networking callbacks without worrying about multiple renders and performance.
If you mutate your stores multiple times synchronously from exotic task sources, multiple renders may happen though. In these rare occasions you can batch changes manually with the batch
function. batch(fn)
executes the passed function immediately and batches any subsequent re-renders until the function execution finishes.
import { h, Component } from 'preact'
import { view, store, batch } from 'z-preact-easy-state'
const user = store({ name: 'Bob', age: 30 })
// this makes sure the state changes will cause maximum one re-render,
// no matter where this function is getting invoked from
function mutateUser() {
batch(() => {
user.name = 'Ann'
user.age = 32
})
}
export default view(() => (
<div>
name: {user.name}, age: {user.age}
</div>
))
NOTE: The React team plans to improve render batching in the future. The batch
function and built-in batching may be deprecated and removed in the future in favor of React's own batching.
Usage with third party components
Third party helpers - like data grids - may consist of many internal components which can not be wrapped by view
, 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 store 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.
import { h, Component } from 'preact'
import { view, store } from 'z-preact-easy-state'
import Table from 'rc-table'
import deepClone from 'clone'
const dataStore = store({
items: [{
product: 'Car',
value: 12
}]
})
export default view(() =>
<Table data={deepClone(dataStore.items)} />
)
How do I derive local stores from props (getDerivedStateFromProps)?
Components wrapped with view
have an extra static deriveStoresFromProps
lifecycle method, which works similarly to the vanilla getDerivedStateFromProps
.
import { h, Component } from 'preact'
import { view, store } from 'z-preact-easy-state'
class NameCard extends Component {
userStore = store({ name: 'Bob' })
static deriveStoresFromProps(props, userStore) {
userStore.name = props.name || userStore.name
}
render() {
return <div>{this.userStore.name}</div>
}
}
export default view(NameCard)
Instead of returning an object, you should directly mutate the passed in 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.
Naming local stores as state
Naming your local state stores as state
may conflict with React linter rules, which guard against direct state mutations. Please use a more descriptive name instead.
Platform support
- 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: iOS 10 and above and Android with community JSC
- IE is not supported and never will be
This library is based on non polyfillable ES6 Proxies. Because of this, it will never support IE.
React Native is supported on iOS and Android is supported with the community JavaScriptCore. Learn how to set it up here. It is pretty simple.
Performance
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 a bit worse than Redux.
How does it work?
Under the hood Easy State uses the @nx-js/observer-util library, which relies on ES6 Proxies to observe state changes. This blog post gives a little sneak peek under the hood of the observer-util
.
Alternative builds
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.
react-easy-state/dist/es.es6.js
exposes an ES6 build with ES6 modules.react-easy-state/dist/es.es5.js
exposes an ES5 build with ES6 modules.react-easy-state/dist/cjs.es6.js
exposes an ES6 build with commonJS modules.react-easy-state/dist/cjs.es5.js
exposes an ES5 build with commonJS modules.
If you use a bundler, set up an alias for react-easy-state
to point to your desired build. You can learn how to do it with webpack here and with rollup here.
Contributing
Contributions are always welcome. Just send a PR against the master branch or open a new issue. Please make sure that the tests and the linter pass and the coverage remains decent. Thanks!