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

https://github.com/kilianc/mozzarella

๐Ÿ‡ฎ๐Ÿ‡น A cheezy-simple hook based immutable store
https://github.com/kilianc/mozzarella

hooks immer react typescript

Last synced: about 2 months ago
JSON representation

๐Ÿ‡ฎ๐Ÿ‡น A cheezy-simple hook based immutable store

Awesome Lists containing this project

README

          










A cheezy-simple 679 bytes hook based immutable store, that leverages useState and Immer to create independent rendering trees so that your components only re-render when they should.


![version](https://img.shields.io/npm/v/mozzarella?style=flat-square)
![size](https://img.shields.io/bundlephobia/minzip/mozzarella?style=flat-square)
![downloads](https://img.shields.io/npm/dm/mozzarella?style=flat-square)




## Motivation

I have been struggling to find a **state management** solution for `react` that makes you interact with your state using plain functions as a baseline. Most of the alternatives I found compromise simplicity, they're verbose or super abstract. I wanted an option that didn't force me to adopt a specific data pattern and was lean.

I don't like boilerplate code. It's the main reason why I stopped using `redux`, but I never stopped chasing most of its design goals. I love how in `redux`, components can be **built in isolation**, **tested easily**, and its overall **separation of concerns**.

While using some of the available `redux` alternatives, I kept asking myself:

* *"Where is the `connect` function?"*.
* *"How do I attach the state to my component without rewriting it?"*.

This led to many awkward implementations attempts, that ultimately fell short one way or another.

I also love **TypeScript**, and it has been hard to find a well balanced solution that satisfied all my requirements as well as having strong type support.

Last but not least: *your state management should be easy to understand for someone that didn't participate in the project design choices*.

### Design Goals

* [x] Be as simple as a **mozzarella** (duh!)
* [x] Use immutability without it getting in the way
* [x] Use plain JS functions as actions
* [x] Use async or sync functions for actions
* [x] Keep actions separated from the store
* [x] Prevent unnecessary re-rendering of components
* [x] Batch changes together to prevent race conditions
* [ ] Batch changes across multiple stores
* [x] Lean and robust `TypeScript` support
* [ ] Add dependencies checks (`react-hooks/exhaustive-deps`) for `useDerivedState` hook
* [ ] Implement concurrency controls similar to [ember-concurrency](http://ember-concurrency.com/docs/task-concurrency)

## Install

$ yarn add --dev --exact mozzarella immer react-fast-compare

## Basic Example ([try it](https://codesandbox.io/s/mozzarella-basic-8og5b?file=/src/index.tsx))

```tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'mozzarella'

// create a store and pass an initial state

const { getState, createAction, useDerivedState } = createStore({
names: ['kilian', 'arianna', 'antonia', 'pasquale'],
places: ['san francisco', 'gavardo', 'salรฒ']
})

// a Immer Draft is passed to the action creator

const addName = createAction((state, name: string) => {
state.names.push(name)
})

const addPlace = createAction((state, name: string) => {
state.places.push(name)
})

// this component only re-renders when `state.names` changes

const Names = () => {
console.info(' re-render')
const names = useDerivedState(state => state.names)

return (


addName('prison mike')}>Add Prison Mike
addPlace('scranton')}>Add Scranton

Names:



    {names.map((name, key) => (
  • {name}

  • ))}

State:


{JSON.stringify(getState(), null, 2)}


)
}

ReactDOM.render(, document.getElementById('root'))
```

## Example with pure functional components ([try it](https://codesandbox.io/s/mozzarella-fc-kwcvh?file=/src/index.tsx))

```tsx
// store.ts

import { createStore } from 'mozzarella'

export const { getState, createAction, useDerivedState } = createStore({
fruits: []
})
```

```ts
// actions.ts

import { createAction } from './store'

export const addFruit = createAction((state, name: string) => {
state.fruits.push(name)
})

export const popFruit = createAction((state) => {
state.fruits.pop()
})
```

```tsx
// fruits.tsx

import React, { FC } from 'react'
import * as actions from './actions'
import { useDerivedState } from './store'

type FruitsProps = {
fruits: string[]
onRemove: () => void
onAdd: (name: string) => void
}

// use this in your component stories and docs
export const Fruits = ({ fruits, onRemove, onAdd }: FruitsProps) => (


Fruits:



    {fruits.map((fruit, key) => (
  • {fruit}

  • ))}

remove last fruit
onAdd('bananas')}>add bananas

)

// use this in your app rendering tree
Fruits.Connected = (() => {
const derivedProps = useDerivedState((state) => ({
fruits: state.fruits,
onAdd: actions.addFruit,
onRemove: actions.popFruit
}))

return
}) as FC
```

```tsx
// index.tsx

import React from 'react'
import ReactDOM from 'react-dom'
import { Fruit } from './fruit'

export const App = () => (

)

ReactDOM.render(, document.getElementById('app'))
```

## API Reference

### `createStore`

```ts
createStore (initialState: S) => {
getState: () => S
useDerivedState: (selector: (state: S) => R) => R
createAction: (actionFn: (state: Draft, ...params: U) => void) => (...params: U) => void
}
```

Takes the initial state as parameter and returns an object with three properties:

* [`getState`](#getState)
* [`createAction`](#createAction)
* [`useDerivedState`](#useDerivedState)

**Example**

```ts
type State = {
users: Record,
photos: Record,
albums: Record,
likes?: Record
}

const { getState, createAction, useDerivedState } = createStore({
users: {},
photos: {},
albums: {}
})
```

---

### `getState`

```ts
const getState = () => S
```

Returns the instance of your immutable state

**Example**

```ts
const { likes } = getState()
```

---

### `createAction`

```ts
const createAction = (actionFn: (state: Draft, ...params: U) => void): (...params: U) => void
```

Takes a function as input and returns a *closured* **action** function that can manipulate a `Draft` of your state.

**Examples**

API call

```tsx
const login = createAction(async (state, email: string, password: string) => {
const {
err,
userId,
apiToken
} = await apiRequest('/auth', { email, password })

state.auth = {
err,
userId,
apiToken
}
})

// ...


{auth.err ?

Error: {err.message}

: null}
login('me@me.com', 'password')}>
login


// ...
```

Nested actions

```tsx
const fetchUsers = createAction(async (state, amount: number) => {
const data = await apiRequest('https://url/data')

data.users.forEach((user) => {
state.users[user.id] = user
})

setPhotos(data.photos)
})

// actions that don't use a draft state directly, can be regular functions
const setPhotos = (photos: Photo[]) => {
photos.forEach(setPhoto)
}

// actions that mutate the state draft, use `createAction`
const setPhoto = createAction((state, photo: Photo) => {
// all mutations in the same tick, use the same draft
// they only trigger a re-render once per tick
state.photos[photo.id] = photo
})
```

Batching state changes

```tsx
const changeName = createAction((state, name: string) => {
state.name = name
})

for (let i = 0; i < 100; i++) {
// each iteration reuses the same state draft
changeName(`name_${i}`)
}

// components subscribed to `state.name` will only re-render once.
// `state.name` will only be set once to "name_99"
```

---

### `useDerivedState`

```ts
const useDerivedState: (selector: (state: S) => R, dependencies?: DependencyList) => R
```

Hook that given a **selector function**, will return the output of the selector and re-render the component only when it changes.

[As per usual](https://reactjs.org/docs/hooks-reference.html#usememo), this hook takes an optional `dependencies` parameter `that defaults to `[]`.

**Example**

```tsx
const UserProfile = (props: { user: User, photos: Photo[] }) => {
return (


User Profile: {props.user.username} ({props.photos.length} photos)



{props.photos.map((photo) => )}


)
}

UserProfile.connected = (props: { userId: string }) => {
const derivedProps = useDerivedState((state) => {
user: state.users[props.userId],
photos: Object.values(state.photos).filter((photo) => photo.userId === props.userId)
}, [props.userId])

return
}
```

Or if you're not being dogmatic about it, or simply not implementing a strict design system:

```tsx
const UserProfile = (props: { userId: string }) => {
const { user, photos } = useDerivedState((state) => {
user: state.users[props.userId],
photos: Object.values(state.photos).filter((photo) => photo.userId === props.userId)
}, [props.userId])

return (


User Profile: {user.username} ({photos.length} photos)



{photos.map((photo) => )}


)
}
```

## How to contribute

Contributions and bug fixes from the community are welcome. You can run the test suite locally with:

$ yarn lint
$ yarn test

## License

This software is released under the MIT license cited below.

Copyright (c) 2020 Kilian Ciuffolo, me@nailik.org. All Rights Reserved.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the 'Software'), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.