Yet another React state manager
Simple, low-impact state manager for smaller React applications.
npm install react-concise-state
importcreateStoreContextfrom"react-concise-state"// 1️⃣ create a store context providing initial state and an actionsconst[context,Provider]=createStoreContext({counter: 0},({ state, setState })=>({// 👇 actions modify state using provided `setState`incrementBy: (increment: number)=>{constnewValue=state.counter+incrementsetState({counter: newValue})},reset: ()=>setState({counter: 0})}))// 2️⃣ wrap component in created providerconstApp=props=>{return<Provider><CounterComponent/></Provider>}// 3️⃣ hook context in consumer to use generated storeconstCounterComponent: React.FC=props=>{conststore=React.useContext(context)// 👇 generated store contains both the state and actions to callconstonIncrement=()=>store.incrementBy(1)constonDecrement=()=>store.incrementBy(-1)return<div><h2>Counter: {store.counter}</h2><hr/><buttononClick={onIncrement}>Increment</button><buttononClick={onDecrement}>Decrement</button><buttononClick={store.reset}>Reset</button></div>}- Store cross-calls
- Middleware
- Multi-paradigm
- Quick and extremely easy to use
- Integrates into general React workflow. Uses contexts, state and hooks
- Low impact. <1kB gziped
- Written in TypeScript
- 100% covered with tests, both for logic and typings
react-concise-state born in frustration and fatigue caused by "modern" React state management. Writing hundreds of boilerplate redux code just to support basic feature gets boring quickly. Newer React features such as context and hooks are there to make state simpler, and this package uses it to make state managing extremely easy, concise and fun. Reducing boilerplate code to zero is the core concept.
Yarn:
yarn add react-concise-state
NPM:
npm install react-concise-state
Make sure you are using recent React version (>=16.8.0) because it works best with it.
support of React >= 16.3.0 is possible. Should it be implemented?
If you are using TypeScript, some of the types might default to any on version <3.2 because of a bug with tuple types.
⭐️Click here to see usage examples⭐️
Below you can find an introduction to the core core concepts of react-concise-state. You will find basic step-by-step walkthrough how to use this package.
Application or application part state can be represented as a plain JavaScript object. For example Counter component state can be defined with such object.
conststate={counter: 0}Now you create a store context.
importcreateStoreContextfrom'react-concise-state'const[context,Provider]=createStoreContext(state)context and Provider are created which you can use in your application to access created store.
context is React.Context, so you can use its Consumer property as you would normally use React Consumer, or instead you can use hooks API.
Examples (click to expand)
Component API
constCounter=props=><context.Consumer>{store=><h1>Current counter: {store.counter}</h1>}</context.Consumer>Hooks API
constCounter=props=>{conststore=React.useContext(context)return<h1>Current counter: {store.counter}</h1>}Provider is a context provider which you should wrap your store consuming components into.
Examples (click to expand)
constApp=props=>{return<Provider><Counter/></Provider>}Note that there should only be one provider for 1 instance of state and consumers might not be the first or only descenders of provider.
constApp=props=>{return<Provider><div><Counter/></div><div><div><OtherCounterWithSameState/></div></div></Provider>}Usually having plain state does not make any sense. There should be some way to modify it. React provides powerfull setState to do that, however using setState for common state on many child components is dangerous and is generally a bad idea. Flux architecture (redux) solves it by defining actions - a contracts telling how it is possible to mutate state, and then defining reducers, sagas, thunks, middleware etc. to actually mutate it. In react-concise-state all those concepts are combined into one in a terse and fluent way.
actions in react-concise-state are plain JavaScript methods which you can call from consumer components to modify current state. Those actions
- Define state mutation contract between store and consumers
- Use native for React
setState - May or may not have a payload
- May or may not return a value
- May be async
- May call own store actions
- May be chained, injected, cached, curried etc.
- May call other stores
- May be written in functional & immutable approach or in imperative approach
To create store action in react-concise-state provide a second argument to createStoreContext - an object where object keys are action names and values are actions themselves.
Basic examples:
// Imperativeconstactions=({state, setState})=>({someAction: (payload)=>{constnewState= ... // do somethingsetState(newState)}})// Functionalconstactions=({setState})=>({someAction: (payload)=>setState(prev=>{..prev,/* do something */})})// Create storecreateStoreContext(state,actions)Those actions will be transformed to store actions which you can call from consumers. In consumers only payload is required argument. Calling this store action will execute the action. {state, setState} wil have real values from provider.
Basic usage examples:
conststore=React.useContext(context)store.someAction('this is a payload string')Advanced (click to expand)
Payload for actions is optional. There could be any amount of payload arguments.
const[context,Provider]=createStoreContext({counter: 0},({state, setState})=>({// No payloadincrement: ()=>setState({counter: ++state.counter}),// If you are using functional style you can also get current state inside `setState` using callback functiondecrement: ()=>setState(state=>({counter: --state.counter})),// With payloadsetValue: (value)=>setState({counter: value}),setValueIfMoreThan: (value,limit)=>{if(state.counter>limit)setState({counter: value})}}))...// Usageconststore=React.useContext(context)store.increment()// > store.counter is 1store.decrement()// > store.counter is 0store.setValue(10)// > store.counter is 10store.setValueIfMoreThan(9,1)// > store.counter is 1You can get return value from actions. It also enabled awaiting async actions.
const[context,Provider]=createStoreContext({todos: []},({setState})=>({// Returning a valueaddTodo: (todo)=>{constresult=Api.addTodo(todo)returnresult},// Getting todos asynchronouslygetTodos: async()=>{consttodos=awaitApi.getTodos()setState({todos})}}))...// Usageconststore=React.useContext(context)constresult=store.addTodo('buy milk')awaitstore.getTodos()You can call actions from other actions.
const[context,Provider]=createStoreContext({todos: []},({setState})=>({addTodo(todo){constresult=Api.addTodo(todo)this.getTodos()},getTodos: async()=>{consttodos=awaitApi.getTodos()setState({todos})}}))...// Usageconststore=React.useContext(context)constresult=store.addTodo('buy milk')// await store.getTodos() - don't need to call it. `.addTodo` will call itSometimes you would like to call other store action from an action. You can't use React.useContext because of specific hook rules in React. Hook amount should never change during runtime and only way to supply that is to initialize all dependency contexts before bootstraping actions.
You can call actions in other stores by providing dependency contexts in a 3rd parameter to createStoreContext. Those contexts will be mapped to corresponding stores internally and will be available in stores object in {setState, action, stores} argument of action creator.
Example:
const[todoContext,Provider]=createStoreContext({todos: []},({state, setState})=>({addTodo: (todo)=>{setState({todos: [...state.todos,todo]})},}))const[mainContext,Provider]=createStoreContext({message: ''},({setState, stores})=>({someAction: (name)=>{const{ todos }=stores.todoContext// stores.todoContext is a "todo store" ({todos: [], addTodo: (todo) => void})constnewMessage=`Hello, ${name}, you have ${todos.length} todos!`setState({message: newMessage})},}),{contexts: { todoContext })
...
// UsageconsttodoStore=React.useContext(todoContext)constmainStore=React.useContext(mainContext)todoStore.addTodo('buy milk')todoStore.addTodo('learn typescript')mainStore.someAction('Dmitrijs')// mainStore.message is "Hello, Dmitrijs, you have 2 todos!" Actions are just a functions which modify state and/or return some values. Actions may be just plain state reducers, or they can contain some complex logic with API calls and data manipulation. In any case you will usually run into such situation, that all actions of the store need to do something the same way. E.g. log input values, handle errors the same way, etc... Middleware is there for that exact reason.
You can provide any middleware to store creation in a 3rd parameter to createStoreContext. That middleware will be executed every time you call any store action, just after you call it and just before it actually executes.
Example:
// Without middlewareconst[todoContext,Provider]=createStoreContext({todos: []},({state, setState})=>({addTodo: (todo)=>{// Loggingconsole.log('Calling addTodo with argument '+todo)// Error handlingtry{Api.addTodo(todo)}catch(ex){console.log(ex)}},getAll: ()=>{// Loggingconsole.log('Calling getAll')// Error handling// Notice how at this point we are writing same stuff over and over againtry{consttodos=Api.getAll()setState({todos})}catch(ex){console.log(ex)}}}))// With middlewareimport{Middleware}from'react-concise-state'// Error handling middlewareconsterrorHandling: Middleware=(next,args,meta)=>{try{// try calling next executable function in the flow (either next middleware or aciton itself)// Don't forget to pass argumentsnext(args)}catch(ex){// If it fails (action or other middleware) log an errorconsole.log(ex)}}// Logging middlewareconstlogging: Middleware=(next,args,meta)=>{// Log to console action key (name) and it's argumentsconsole.log(`Calling ${meta.actionName} with arguments ${args}`)// Don't forget to call `next(args)`! next(args)}const[todoContext,Provider]=createStoreContext({todos: []},({state, setState})=>({addTodo: (todo)=>Api.addTodo(todo),getAll: ()=>{consttodos=Api.getAll()setState({todos})}}),{middleware: [errorHandling,logging]})// Provide middleware to the storeMiddleware is a useful pattern, which you can use to streamline store actions, make stores more generic and have almost perfect reusability across contexts.
Default and the easiest way to create a middleware for your store is to make a new function of type Middleware.
However, what if you want to save every exception into some store and then display those errors in some other components nicely? You may inject stores into middleware by using
createMiddleware helper. After injecting stores you may access store state and actions inside middleware. Error handling middleware example:
import{createMiddleware,createStoreContext}from'react-concise-state'// Creating errors storeconst[context,Provider]=createStateContext({latestError: nullasError|null,errorLog: []asError[]},({setState})=>{// Set latestError and push it to error loghandleError: (error: Error)=>{setState(prev=>({...prev,latestError: error,errorLog: [...prev.errorLog,error]}))}})// Creating error handling middleware with injected errors storeconsterrorHandling=createMiddleware((next,args,meta)=>{try{awaitnext(args)}catch(ex){// Call error store to save error in itmeta.stores.errors.handleError(ex)}},{errors: context})Any settings or additional store information which might be needed can be stored in special meta option of the store creator. This meta information is unchanged through store lifetime, is available for every action and middleware. Meta type is a dictionary of user-defined values. The most common usage for metadata is providing API url/token, dev/prod flags or anything else which is static through application lifetime. Example:
// This file will export correct baseUrl and headers for authentication base on the environment (DEV/TEST/PROD)import{baseUrl,authHeaders}from'./config'const[context,Provider]=createStateContext({todos: []},({setState, meta})=>{getAll: async()=>{// Use provided meta data. It is alternative way of using those values from global scopeconstres=awaitfetch(meta.baseUrl,{headers: meta.headers})consttodos=awaitres.json()setState({todos})}}.{// Provide values through meta option
meta: {baseUrl: baseUrl,headers: authHeaders,// Flag to tell logging middleware that this store must not be logged shouldNotLog: true},middleware: [logging]})// Logging middlewareconstlogging: Middleware=(next,args,meta)=>{// Check meta data to see if this store should not be loggedif(meta.shouldNotLog)returnnext(args)console.log(`Calling ${meta.actionName} with arguments ${args}`)next(args)}This library is written in TypeScript and leverages its type system to the fullest. One of the main goals of this library is to provide type-safe state management with minimum (almost zero) boilerplate code.
Why most libraries fail on this
TypeScript is really powerful. It's type system is so flexible yet so smart ([turing-complete smart](microsoft/TypeScript#14833)) that it is a shame very few developers and libraries use it to the fullest.
TypeScript is able to infer and calculate most of the types itself, yet libraries still require developers to write interfaces, implement contracts, provide types for every single bit of functionality. TypeScript should guide towards correct implementation, not hinder from incorrect one.
You can use this library without writing any type and you will still have perfect type-safety and type-correctness. Types will be automatically resolved and given to you so you are safe about your implementation.
When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState
You can also provide TState type to constrain initial state or narrow state types.
When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.
After describing your store with createStoreContext you will be possible to resolve store using React.useContext hook. Resulting store will be an intersection of stateand mapped actions.
You can set additional action arguments and return any value. Mapped action will infer all of that and provide it to you.
📖 Read full api reference and docs by clicking here 📖
MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.



