Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/samuelgja/oustate
👀 Another React state management library
https://github.com/samuelgja/oustate
hooks react react-native state-management
Last synced: about 2 months ago
JSON representation
👀 Another React state management library
- Host: GitHub
- URL: https://github.com/samuelgja/oustate
- Owner: samuelgja
- Created: 2022-10-12T20:58:25.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-07-31T14:07:29.000Z (5 months ago)
- Last Synced: 2024-10-02T00:18:41.509Z (3 months ago)
- Topics: hooks, react, react-native, state-management
- Language: TypeScript
- Homepage:
- Size: 493 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
### Oustate
[![Build](https://github.com/samuelgjabel/oustate/actions/workflows/build.yml/badge.svg)](https://github.com/samuelgjabel/oustate/actions/workflows/build.yml) [![Code quality check](https://github.com/samuelgjabel/oustate/actions/workflows/code-check.yml/badge.svg)](https://github.com/samuelgjabel/oustate/actions/workflows/code-check.yml)
[![Build Size](https://img.shields.io/bundlephobia/minzip/oustate?label=Bundle%20size)](https://bundlephobia.com/result?p=oustate)👀 Another small, fast but robust ☝️ **React state management library** with aim 🎯 on simplicity and scalability in real world scenarios.
Still experimental / beta. **Do not use it in productions yet!** 👻
Based on React [hooks](https://reactjs.org/docs/hooks-reference.html) api. Inspired by [recoil](https://recoiljs.org/) and [zustand](https://github.com/pmndrs/zustand/blob/main/readme.md) while try to achieve the best from these two worlds.
Solving problems like the dreaded [zombie child problem](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children), [react concurrency](https://reactjs.org/blog/2022/03/29/react-v18.html) and [context loss](https://github.com/facebook/react/issues/13332) between mixed renderers with focus on **re-renders reduction**.
- [install](#install)
- [quick start](#quick-start)
- [api](#api)
- [babel transform plugin](#babel-plugin)
- [slicing guide](#slicing-guide)#### Install
```bash
yarn add oustate # or npm i oustate
```#### Quick Start
Simple state example
[live demo](https://codesandbox.io/s/oustate-example-kj2ubq?file=/src/App.tsx)```typescript
import { createState, useStateValue } from 'oustate'const userState = createState({ username: 'John', age: 30 })
const App = () => {
const user = useStateValue(userState) // return all the user object
return (
userState.set((user) => {
user.age++
return { ...user }
})
}>
{user.age}
)
}
```Simple state example with care about re-renders
[live demo](https://codesandbox.io/s/oustate-re-render-count-ggbgoq?file=/src/App.tsx)```typescript
import { createState, useStateValue } from 'oustate'const userState = createState({ username: 'John', age: 30 })
const App = () => {
const userAge = useStateValue(userState, user => user.age) // return only user.age, so this component re-render only if user.age is changed.
userState.set((user) => {
user.age++
return { ...user }
})
}>
{userAge}
)
}
```Simple computed example
[live demo](https://codesandbox.io/s/oustate-computed-ef8ozl?file=/src/App.tsx)Simple computed async example
[live demo](https://codesandbox.io/s/oustate-async-computed-sl4h55?file=/src/App.tsx)#### API
##### `createState`
creating basic atom state - it can be almost any value - object / atom / ...
**Types**
```typescript
export interface StateOptions {
isSame?: IsSame
onSet?: (oldValue: T, setCallback: () => T) => T
}
/**
* Creating of basic atom state.
* @param defaultState - any value
* @param options - optional options for state (isSame, onSet)
* @returns AtomState
*/
export declare const createState: (
defaultState: StateInternal,
options?: StateOptions> | undefined,
) => AtomState>
```**Example:**
```typescript
import { createState } from 'oustate'const defaultState = 2
const state = createState(defaultState, {
// options are optional
isSame: (prev, next) => true,
onSet(oldValue, setCallback) {
const newValue = setCallback()
console.log(oldValue, newValue)
},
})// get state out of react scope
state.get()// set new state
state.set(3)// use state in react scope
const stateValue = useStateValue(state)
```##### `createStateFamily`
same as `createState` but instead of returning `AtomState`, it **returns function** where first parameter (key) is unique state identifier and returns `AtomState`
**Types**
```typescript
export interface StateOptions {
isSame?: IsSame
onSet?: (oldValue: T, setCallback: () => T) => T
}
/**
* Create atom family state. It's same as createState but instead of return `AtomState` it returns `AtomFamily`.
* `AtomFamily` is function that accepts `key` and returns `AtomState`.
*/
export declare const createStateFamily: (
defaultState: StateInternal,
options?: StateOptions> | undefined,
) => AtomFamily>
```**Example:**
```typescript
import { createStateFamily } from 'oustate'const defaultState = 2
const state = createStateFamily(defaultState, {
// options are optional
isSame: (prev, next) => true,
onSet(oldValue, setCallback) {
const newValue = setCallback()
console.log(oldValue, newValue)
return newValue
},
})// get state out of react scope
state('some-key').get()// set new state
state('some-key').set(3)// use state in react scope
const stateValue = useStateValue(state('some-key'))
```##### `createComputed`
computed state is a state that depends on other states or other computed states. It is recomputed when the states it depends on change.
**It can be also async****Types**
```typescript
interface GetSelectionOptionsBase {
get: GetState
abortSignal?: AbortSignal
isCanceled: () => boolean
key?: Key
}
export declare type GetSelectionOptions = GetSelectionOptionsBase & T
/**
* Computed state is a state that depends on other states or other computed states.
* It is recomputed when the states it depends on change.
* **It can be also async**.
*/
export declare const createComputed: (
getSelection: (options: GetSelectionOptions) => StateInternal | Promise>,
options?: ComputedOptions> | undefined,
) => ComputedState>
```**Example:**
```typescript
import { createState, createComputed } from 'oustate'const counterState = createState(0)
const userState = createState({ name: 'John', age: 20 })// creating computed depends on counterState & userState
const counterPlusUserAgeState = createComputed(({ get }) => get(counterState) + get(userState, (user) => user.age))
// get state
await counterPlusUserAgeState.get()// react scope
const counterPlusUser = useStateValue(counterPlusUserAgeState)
```_Note: Keep in mind when using **useStateValue** and **async computed state**, component need to be wrapped into the [Suspense](https://reactjs.org/docs/react-api.html#reactsuspense)! For more control over `computed` loading states use `useLoadableStateValue` instead `useStateValue`_
##### `createComputedFamily`
same as `createComputed`, but instead of returning `ComputedState`, it **returns function** where first parameter (key) is unique state identifier and returns `ComputedState`
**Example:**
```typescript
import { createState, createComputed } from 'oustate'const counterState = createState(0)
const userState = createState({ name: 'John', age: 20 })// creating computed depends on counterState & userState
const counterPlusUserAgeState = createComputedFamily(({ get }) => get(counterState) + get(userState, (user) => user.age))
// get state
await counterPlusUserAgeState('key').get()// react scope
const counterPlusUser = useStateValue(counterPlusUserAgeState('key'))
```##### `createSlice`
it's just helper function - slice wrapped around `createComputed`.
There are scenarios when need to slice 1 state in same way in multiple components (`const userAge = useStateValue(userState, user => user.age) `),
instead of writing same logic multiple times in react scope,
`createSlice` helps to bring it to the global scope (`const userAgeState = createSlice(userState, user => user.age) `).**Example:**
```typescript
import { createState, createSlice } from 'oustate'const userState = createState({ name: 'John', age: 20 })
const userAgeState = createSlice(userState, (user) => user.age)
// get state
await userAgeState.get()// react scope
const counterPlusUser = useStateValue(userAgeState)
```Passing functions into the state (like setting state) is not recommended.
_Note: State need to be used in **global js context** (🤫 it can be used also in `React` context, but carefully!)_
**Hooks:**
After state is created you can easily use it with `useStateValue`, `useLoadableStateValue` or `useCachedStateValue` hooks.
##### `useStateValue`
hook for getting state value - when async computed is used - need wrap component into the suspense.
##### `useLoadableStateValue`
hook for getting state value but with more control over loading state - component don't need to be wrapped into the suspense
##### `useCachedStateValue`
hook for getting state with caching control - it's useful for async computed states - when on first load it went to the suspense, but on second change it will returns loading state + old state
#### babel-plugin
`oustate-babel-transform-plugin` - For lazy persons 🥱😴
- there is also babel plugin to avoid re-renders without defining areEqual function manually - it will generates compare function automatically.
_if there is manually defined compare function - plugin will ignore it._
_if there is defined custom slice selector - plugin will respect it._for example something like this:
```typescript
import { useStateValue } from 'oustate'// without defining slice and return just atom value or define compare function manually - component will re-render on each time when any property from userState will change
const { username } = useStateValue(userState)
const { name } = useStateValue(someState, (state) => ({ name: state.notNameProperty }))
```will be automatically converted to something like this:
```typescript
import { useStateValue } from 'oustate'// function below is automatically generated by oustate-babel-transform-plugin
function isSameCompare1(prev, next) {
if (prev.username !== next.username) {
return false
}
return true
}
function isSameCompare2(prev, next) {
if (prev.name !== next.name) {
return false
}
return true
}const { username } = useStateValue(userState, undefined, isSameCompare1) // component only re-render when username is changed.
const { name } = useStateValue(someState, (state) => ({ name: state.notNameProperty }), isSameCompare2) // useful when creating new references from slice selector.
```##### install babel plugin:
```bash
yarn add -D oustate-babel-transform-plugin # or npm i -D oustate-babel-transform-plugin
```##### using babel plugin
update plugins in your `babel.config.js` or `.babelrc` file
```javascript
module.exports = {
plugins: ['oustate-babel-transform-plugin'],
}
```Plugin works only for oustate hooks, when directly access object properties or array properties:
- **ObjectPattern** (`const {some} = useStateValue(state)`)
- **ArrayPattern** (`const [some, value] = useStateValue(state)`)here are cases which will be not transformed:
```typescript
const someState = useStateValue(state)
const { value } = someState // transform will be ignored because it's not called directly from useStateValue hook.
``````typescript
const someState = useStateValue(state)
someState.value // transform will be ignored because it's not called directly from useStateValue hook.
```#### Slicing guide
to avoid re-renders, state can be sliced in `react scope`, `computed scope`.
- **react-scope**
example how to slice state and avoid re-renders when not used values are changed:```typescript
const counter = useStateValue(userState, (state) => state.counter) // it will pick the counter property from userState object, so only if counter from state will change, this component will re-render
```example when slice will not work:
```typescript
const { counter } = useStateValue(userState, (state) => ({ counter: state.counter })) // here is the issue, because while creating new slice, it also create new object reference `({counter:something})`
```In this example component will re-render on each userState change, because instead of picking state data directly, it's added to the new object reference.
Creating new references with slice / selector will always **cause re-renders**.
There is fix around it and it's third parameter in hooks - equality check.
That's why [babel transform plugin](#babel-plugin) exist.
Also there can be used third party libraries to equality check like [react-fast-compare](https://github.com/FormidableLabs/react-fast-compare).example with equality check:
```typescript
const isSame = (prev, next) => {
if (prev.counter !== next.counter) {
return false
}
return true
}const { counter } = useStateValue(userState, (state) => ({ counter: state.counter }), isSame) // equality function comparing always object from selector, if provided, otherwise state.
```- **computed-scope**
example how to slice state in computed and avoid re-renders & computed calls when not used values are changed:```typescript
const computedCounter = createComputed(({ get }) => {
return get(userState, (state) => state.counter) // slicing the state in computed
})
```Keep in mind that using slices / selectors in all hooks **don't need to be memoized!**
[Selectors no longer need to be memoized](https://github.com/reactwg/react-18/discussions/86)**👋 Welcome back inline functions 👋**
Well `tested`, written in `typescript`.
This library is just playing around with possible solutions how to create react states, so if you can look at the code & give some feedback - positive or negative, I will appreciate it! 🤗