An open API service indexing awesome lists of open source software.

https://github.com/sultan99/holycow

๐Ÿฎ React hook-based state management
https://github.com/sultan99/holycow

fp hook javascript react state-management

Last synced: about 1 year ago
JSON representation

๐Ÿฎ React hook-based state management

Awesome Lists containing this project

README

          

# Holy State
[![codecov](https://codecov.io/gh/sultan99/holycow/branch/main/graph/badge.svg)](https://codecov.io/gh/sultan99/holycow)
![npm bundle size](https://img.shields.io/bundlephobia/minzip/%40holycow%2Fstate?label=gzip%20size&color=green&link=https%3A%2F%2Fbundlephobia.com%2Fpackage%2F%40holycow%2Fstate%401.2.0)
![NPM Downloads](https://img.shields.io/npm/d18m/%40holycow%2Fstate?link=https%3A%2F%2Fnpmtrends.com%2F%40holycow%2Fstate)
![GitHub License](https://img.shields.io/github/license/sultan99/holycow?link=https%3A%2F%2Fgithub.com%2Fsultan99%2Fholycow%2Fblob%2Fmain%2FLICENSE)

> Hook-based state management library for [React](https://github.com/facebook/react) applications.













## Quick intro
Holy moly, you are here! You're more than welcome!

So, it is all about state management handled by hooks. Think of it as a utility for creating hooks that can store a global state across the entire application. The coolest part is that it works without context providers, observables, selectors, or HOC connectors. No boilerplate code but hooks.

### ๐Ÿฆ„ Main features
- The library is tree-shakeable with no external dependency. [Gzip size: ~1.9kb](https://bundlephobia.com/package/@holycow/state@1.2.0).
- The state hooks can be used outside of the React tree.
- Greedy rendering. Only updated values trigger component rendering.
- Computed values with caching and hook nesting.
- Asynchronous actions.
- Subscription to the state changes.
- Event-driven architecture support.
- Friendly with functional programming.
- Strongly typed with TypeScript.







### ๐Ÿš€ Getting started
- ๐Ÿ“– [Online documentation](https://holy-cow.gitbook.io/holy-state/)
- ๐Ÿฟ [Online demo](https://codesandbox.io/s/github/sultan99/cards/tree/main)
- ๐Ÿ™ [Demo code source](https://github.com/sultan99/cards)

```sh
npm install @holycow/state
```

```jsx
import {createState} from '@holycow/state'

// ๐Ÿ‘‡ your store is a hook!
const useUser = createState({
id: 1,
name: 'Homer Simpson',
address: {
house: 742,
street: 'Evergreen Terrace',
},
})

const UserName = () => {
const {name} = useUser() // ๐Ÿ‘ˆ value from the state
return

{name}

}

const {id, name, address} = useUser
// any values ๐Ÿ‘† from the hook can be used outside of components
```

### ๐Ÿƒ State update
It is quite simple to modify state values with the built-in `set` function.

```js
const {set} = useUser()

set('name', 'Ovuvuevuevue') // key & value
set('id', prevId => prevId + 1) // key & function
set('address.street', 'rue de Beggen') // path & value, no spreading objects

// atomic updates: multiple values at the same time
set(state => ({
...state,
id: state.id + 1,
name: 'Ovuvuevuevue',
}))
// or object as updating input
set({
id: prevId => prevId + 1,
name: 'Ovuvuevuevue',
})
```

The `set` function is not only overloaded but curried as well. We can apply parameters to it partially one by one:

```js
const setId = set('id') // returns function which will update 'id'
setId(2) // actual updates with a value
setId(prevId => prevId + 1) // or using function

fetch('/api/users/1/address')
.then(res => res.json())
.then(set('address')) // equals ๐Ÿ‘‰ .then(data => set('address', data))
```

### ๐ŸŽฌ Actions
An action is any piece of code that modifies the state and targets some specific task, unlike the `set` function, which is more generic and used for a simple value update. It is a good place to move business logic like validation or network operations.

Action expects a curried function as a parameter. The first function provides the state โ€” the second one handles the action payload.

```jsx
import {createState, action} from '@holycow/state'

const useAuth = createState({
token: '',
error: '',
loading: false, // ๐Ÿ‘‡ state ๐Ÿ‘‡ payload
login: action(({set, loading}) => formData => {
if (loading) return
set('error', '') // ๐Ÿ‘ˆ the state can be updated directly from the action
set('loading', true)
fetch('/api/login', {method: 'POST', body: formData})
.then(res => res.json())
.then(set('token'))
.catch(set('error'))
.finally(() => set('loading', false))
}),
})

// โšก handler is created outside of the component, +1 performance
const handleSubmit = event => {
event.preventDefault()
useAuth.login(new FormData(event.target))
}

const Login = () => {
const {loading, error} = useAuth()
return (



{error}


{loading ? 'Submitting' : 'Login'}


)
}
```

### ๐Ÿง  Smart rendering
Unlike other state management, the holy state library does not require memoized selectors or further optimization to avoid unnecessary rerenders. Instead, it comes with a state tracking feature out of the box. Only components with altered values get rerendered.

```jsx
const Street = () => {
const {address} = useUser()
return

{address.street}
// Evergreen Terrace
}

// ๐Ÿ’ค no render even the address object was updated, +1 performance
useUser.set('address.house', 10)
// ๐Ÿ’ค no render, equal value applied, +1 performance
useUser.set('address.street', 'Evergreen Terrace')
// ๐Ÿƒโ€โ™‚๏ธ now it will be rendered
useUser.set('address.street', 'Spooner')
```

### ๐Ÿงฎ Computed values
A computed value is a value returned by a specified function. The function's input can be a state value or any other hook. To avoid unnecessary computations, the computed value is cached and recomputed only when the current dependency has changed. Conceptually, computed values are similar to spreadsheets' formulas or Redux memoized selectors.

```jsx
import {createState, computed} from '@holycow/state'

const useUser = createState({
name: 'Peter',
birthDate: {
day: 8,
month: 12,
year: 1979,
},
// ๐Ÿฆฅ lazy evaluation, function will be called when the value is used
age: computed(state =>
new Date().getFullYear() - state.birthDate.year
),
})

// usage
const UserAge = () => {
const {name, age} = useUser()
// here 'age' ๐Ÿ‘† value is calculated and cached
return

{name} is {age} years old guy.

}

const homerAge = userUser.age // ๐Ÿ‘ˆ value from the cache, +1 performance
```

In the example above, the computed value age will be recalculated if the year of birthDate is modified. Otherwise, it will use the cached value.

What if we want to keep our `age` value updated when the year is changed? Let's assume our hard-working QA engineer opens our app on 31 December at 11:58!

We could use the [useCurrentYear](https://gist.github.com/sultan99/8ad653259263e052951f9d961d5d982e) hook to keep the value updated, respectively. Then we should wrap the hook with the side effect function.

```js
const useUser = createState({
name: 'Peter',
birthDate: {
day: 8,
month: 12,
year: 1979,
}, // side effect function ๐Ÿ‘‡
age: computed((state, sideEffect) =>
sideEffect(useCurrentYear) - state.birthDate.year
// when it's required to pass a parameter ๐Ÿ‘‡
// sideEffect(() => useCurrentYear('some-params'))
),
})
```

We should remember by using side effects, we lose the benefit of caching.

### ๐Ÿคน Selectors
Selectors are designed for convenient state access. The selector retrieves a value from the state at a given path. If we query more than one value, the selector will return an array with the requested values.

```jsx
const useMessages = createState({
author: {
id: 1,
name: 'Peter',
},
messages: [
{id: 10, text: 'Hello'},
{id: 20, text: 'World!'},
]
})

// single value
const authorName = useMessages('author.name')
// ๐Ÿ‘† equivalent ๐Ÿ‘‡
const {author} = useMessages()
const authorName = author.name

// multiple values
const [authorName, firstMessage] = useMessages('author.name', 'messages.0.text')
// ๐Ÿ‘† equivalent ๐Ÿ‘‡
const {author, messages} = useMessages()
const authorName = author.name
const firstMessage = messages[0].text

// one line component with a selector ๐Ÿ‘‡
const AuthorName = () =>

{useMessages('author.name')}

```

The same trick we can do with actions or `set` functions:
```js
const setUser = useUser('set')
const setMessage = useMessages('set')
// ๐Ÿ‘† equivalent ๐Ÿ‘‡
const {set: setUser} = useUser()
const {set: setMessage} = useMessages()
```

### ๐Ÿ—ƒ๏ธ Context state
Context state is a multiple-instance state with its own scope. It is designed to create reusable nested components that share one state with their child components. It is similar to the React Context, but powered by holy state features.

- Greedy rendering, meaning that only updated values trigger component rendering.
- Computed values with caching and hook nesting.
- Asynchronous actions.

However, the state hook can only be used inside React components and does not support subscription to the state changes.

The `createContextState` function creates a state and returns a tuple with a context provider and a hook. The initial context state can be overridden by a parent component. This allows you to create uncontrolled, controlled, or partially controlled components.

```jsx
import {action, createContextState} from '@holycow/state'

const [Context, useCounter] = createContextState({
// ๐Ÿ‘‡ the context initial state
count: 0,
name: `Untitled`,
increment: action(({set}) => () =>
set(`count`, value => value + 1)
),
})

const Label = () => {
// same as the holy state hook ๐Ÿ‘‡
const {name, count} = useCounter()
return

{name}: {count}


}

const Button = () => {
const {increment} = useCounter()
return (

Increment

)
}

// only assigned props {name, count, increment}
// will override the initial context state
// undefined props ๐Ÿ‘‡ will be ignored
const Counter = props => (




)

const App = () => {
const [age, setAge] = useState(21)
return (

// each Counter component will have it is own state

setAge(age + 1)}/>
// ๐Ÿ‘† controlled component by parent component

)
}
```

### ๐Ÿ“ฌ State subscriptions
We can subscribe to the state changes and get notified when the state is updated. The `subscribe` function accepts a callback function as a parameter and returns another function for unsubscription.

```js
// subscription to the whole state ๐Ÿ‘‡
const unsubscribe = useUser.subscribe(state => {
localStorage.setItem('user', JSON.stringify(state))
})

unsubscribe() // canceling subscription ๐Ÿ‘†

// subscription to specific ๐Ÿ‘‡ value
useUser.subscribe('address.street', street => {
console.log(`User street was changed to ${street}`)
})
```

### ๐Ÿ“ข Signal Events
Signals provide a simple way to communicate between decoupled hooks that don't know each other directly, but some of them wait for the other to occur to do something. So, for example, we could import a user profile state in lazy mode when the user gets logged in. But before that, we fetch only the required hooks to handle the guest state. On the other hand, it avoids tight coupling of hooks and can resolve circle dependencies issues. Shortly, the signals are the implementation of event-driven architecture.

> It is optional to use the signals. Subscriptions and nesting hooks can provide the same functionality.

There are three steps to use the signals:
- Creation of the emitter function: `const ringDoorbell = createSignal()`.
- Creation of the signal listener: `on(ringDoorbell, useDoor.open)`.
- Call `ringDoorbell()` function to trigger the action.

```js
import {createSignal, on} from '@holycow/state'

const logout = createSignal() // ๐Ÿ‘ˆ creates logout signal function

// auth.js
on(logout, () => { // ๐Ÿ‘ˆ listens to the logout signal
useAuth.logout()
console.log('Bye bye!')
})

// user.js
on(logout, () => {
useUser.reset() // built-in function that restore initial state of the hook
localStorage.removeItem('user')
})

// ๐Ÿ‘‡ emits the logout signal
logout()
```

To disable the listener, we should call the function returned by the `on` function.

```js
const off = on(login, useAuth.login)

login('homer@simpson.com', 'pa$$word')

off() // ๐Ÿ‘ˆ stops to react on the login signal
```

Signals can be executed once and then removed from the listeners.

```js
import {createSignal, once} from '@holycow/state'

const init = createSignal()

// ๐Ÿ‘‡ instead of 'on' we use 'once'
once(init, () => {
usePosts.loadPosts()
})

init() // ๐Ÿ‘ˆ will trigger the callback function
init() // no effects
```

### ๐Ÿ“Ž TypeScript
The state typing is designed to be seamless. Once it is typed, it should provide the correct types everywhere.

```tsx
import type {Action, Computed, Computed} from '@holycow/state'
import {createState} from '@holycow/state'

type Todo = {
id: number
checked: boolean
description: string
}
/**
* Computed
* Action action with no payload
* Action
*/
type TodosState = {
filter: 'all' | 'completed' | 'uncompleted'
todos: Todo[]
filteredTodos: Computed // ๐Ÿ‘ˆ computing function returns Todo[]
addTodo: Action // ๐Ÿ‘ˆ action with payloads
clearTodos: Action // ๐Ÿ‘ˆ action without payload
}
// ๐Ÿ‘† TypeScript zone, it can even be in a separate file.
const useTodos = createState({
// ๐Ÿ‘‡ below like a normal JS code
filter: 'all',
todos: [
{id: 1, checked: true, description: 'Buy milk'},
{id: 2, checked: false, description: 'Clean room'},
],
filteredTodos: computed(state => {
const {filter, todos} = state
const isAll = filter === 'all'
const isCompleted = filter === 'completed'

return isAll ? todos : todos.filter(
({checked}) => checked === isCompleted
)
}),
addTodo: action(state => (description, checked = false) => {
const {set, todos} = state
const id = todos.reduce((acc, {id}) => Math.max(id + 1, acc), 0)
const newTodo = {id, description, checked}

set('todos', [...todos, newTodo])
}),
clearTodos: action(({set}) => () => {
set('todos', [])
})
})

const [addTodo, set, todo] = useTodos('addTodo', 'set', 'todos.0') // โœ… all good
const [addTodo, set, todo] = useTodos('addtodo', 'set', 'todos.0') // โŒ type error
// ๐Ÿ‘† typos

todo?.description // โœ… all good
todo.description // โŒ type error, it might be undefined

addTodo('Buy milk') // โœ… all good
addTodo(123) // โŒ type error

set('filter', 'completed') // โœ… all good
set('filter', 'new') // โŒ type error

const setFilter = set('filter') // curried function
setFilter('completed') // โœ… all good
setFilter('new') // โŒ type error
```