Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

react-concise-state

npmTravisCodecov

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>}

Features

  • 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

Intro

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.

Installation

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.

Examples

⭐️Click here to see usage examples⭐️


Core Concepts

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.

createStoreContext - creating store context

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 - consuming created context

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>}

actions - modifying state

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 1

You 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 it

contexts - calling other stores

Sometimes 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!" 

Wrapping store actions in a middleware

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 store

createMiddleware - creating custom store-bind middleware

Middleware 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})

meta - store metadata

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)}

TypeScript

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.

How types in this library work

1. initialState

When creating a new store context initial state could be anything. Resulting state will be infered from provided initialState

Initial state infered demo

You can also provide TState type to constrain initial state or narrow state types.

Initial state constrained demo

2. actions

When creating store actions you will be provided with correct types for current state, setState method and stores and meta objects.

Action types provided demo

3. Mapped actions

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.

Action types provided demo

Docs

📖 Read full api reference and docs by clicking here 📖


MIT License Copyright (C) Dmitrijs Minajevs dmitrijs.minajevs@outlook.com.

About

Yet another react state manager

Topics

Resources

Code of conduct

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages