Ecosyste.ms: Awesome

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

Awesome Lists | Featured Topics | Projects

https://github.com/franciscop/statux

⚛️ The easy state management library for React with Hooks
https://github.com/franciscop/statux

Last synced: 6 days ago
JSON representation

⚛️ The easy state management library for React with Hooks

Awesome Lists containing this project

README

        

# Statux [![npm install statux](https://img.shields.io/badge/npm%20install-statux-blue.svg "install badge")](https://www.npmjs.com/package/statux) [![test badge](https://github.com/franciscop/statux/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/statux/blob/master/.github/workflows/tests.yml) [![gzip size](https://badgen.net/bundlephobia/minzip/statux?label=gzip&color=green "gzip badge")](https://github.com/franciscop/statux/blob/master/index.min.js)

The easy state management library with [React Hooks](#react-hooks) and [immutable state](#truly-immutable):

User Profile example screenshotApp example screenshot

It allows you to share state across different components of your WebApp with a simple and clean syntax. This [reduces a lot of boilerplate](#direct-manipulation) so you can focus on the actual app that you are building.

Jump to docs for [``](#store), [`useStore()`](#usestore), [`useSelector()`](#useselector), [`useActions()`](#useactions), [_examples_](#examples).

## Getting started

First create a React project (try [Create-React-App](https://github.com/facebook/create-react-app)) and install `statux`:

```
npm install statux
```

Then initialize the store at the App.js level with a couple of initial values:

```js
// src/App.js
import Store from "statux"; // This library
import Website from "./Website"; // Your code

// Initial state is { user: null, books: [] }
export default () => (



);
```

Finally, use and update these values wherever you want:

```js
// src/User.js
import { useStore } from "statux";

export default () => {
const [user, setUser] = useStore("user");
const login = () => setUser({ name: "Maria" });
return (

Hello {user ? user.name : Login}

);
};
```

## API

There are four pieces exported from the library:

- [**``**](#store): the default export that should wrap your whole App. Its props define the store structure.
- [**`useStore(selector)`**](#usestore): extracts a part of the store for data retrieval and manipulation. Accepts a parameter to specify what subtree of the state to use.
- [**`useSelector(selector)`**](#useselector): retrieve a specific part of the store state based on the selector or the whole state if none was given.
- [**`useActions(selector)`**](#useactions): generate actions to modify the state while avoiding mutations. Includes default actions and can be extended.

### \

This should wrap your whole project, ideally in `src/App.js` or similar. You define the structure of all of your state within the ``:

```js
// src/App.js
import Store from "statux";
import Navigation from "./Navigation";

// state = { id: null, friends: [] }
export default () => (



);
```

When your state starts to grow - but not before - it is recommended to split it into a separated variable for clarity:

```js
// src/App.js
import Store from "statux";
import Navigation from "./Navigation";

const initialState = {
id: null,
friends: [],
// ...
};

export default () => (



);
```

That's all you need to know for creating your state. When your app starts to grow, best-practices of redux like normalizing your state are recommended.

### useStore()

This is a [React hook](https://reactjs.org/docs/hooks-overview.html) to handle a state subtree. It accepts **a string selector** and returns an array similar to [React's `useState()`](https://reactjs.org/docs/hooks-state.html):

```js
import { useStore } from "statux";

export default () => {
const [user, setUser] = useStore("user");
return (

setUser({ name: "Maria" })}>
{user ? user.name : "Anonymous"}

);
};
```

You can access deeper items and properties within your state through the selector:

```js
import { useStore } from "statux";

export default () => {
// If `user` is null, this will throw an error
const [name = "Anonymous", setName] = useStore("user.name");
return

setName("John")}>{name}
;
};
```

It accepts a _string_ selector that will find the corresponding state subtree, and also return a modifier for that subtree. `useStore()` behaves as the string selector for `useSelector()` and `useActions()` together:

```js
const [user, setUser] = useStore("user");
// Same as
const user = useSelector("user");
const setUser = useActions("user");
```

> Note: useStore() **only** accepts either a string selector or no selector at all; it **does not** accept ~~functions~~ or ~~objects~~ as parameters.

The first returned parameter is the frozen selected state subtree, and the second parameter is the setter. This setter is quite flexible:

```js
// Plain object to update it
setUser({ ...user, name: "Francisco" });

// Function that accepts the current user
setUser((user) => ({ ...user, name: "Francisco" }));

// Modify only the specified props
setUser.assign({ name: "Francisco" });
```

See the details and list of helpers on [the `useActions()` section](#useactions).

### useSelector()

This React hook retrieves a frozen (read-only) fragment of the state:

```js
import { useSelector } from "statux";

export default () => {
const user = useSelector("user");
return

{user ? user.name : "Anonymous"}
;
};
```

You can access deeper objects with the dot selector, which works both on objects and array indexes:

```js
import { useStore } from "statux";

export default () => {
const title = useSelector("books.0.title");
const name = useSelector("user.name");
return (


{title} - by {name}

);
};
```

It accepts both a _string selector_ and a _function selector_ to find the state that we want:

```js
const user = useSelector("user");
const user = useSelector(({ user }) => user);
const user = useSelector((state) => state.user);
```

You can dig for nested state, but if any of the intermediate trees is missing then it will fail:

```js
// Requires `user` to be an object
const name = useSelector("user.name");

// Can accept no user at all:
const user = useSelector(({ user }) => (user ? user.name : "Anonymous"));

// This will dig the array friends -> 0
const bestFriend = useSelector("friends.0");
```

### useActions()

This React hook is used to modify the state in some way. Pass a selector to specify what state fragment to modify:

```js
const setState = useActions();
const setUser = useActions('user');
const setName = useActions('user.name');

// Update in multiple ways
setName('Francisco');
setName(name => 'San ' + name);
setName((name, key, state) => { ... });
```

These actions must be executed within the appropriate callback:

```js
import { useActions } from "statux";
import Form from "your-form-library";

const ChangeName = () => {
const setName = useActions("user.name");
const onSubmit = ({ name }) => setName(name);
return ...;
};
```

There are several helper methods. These are based on/inspired by the array and object prototype linked in their names:

- [`fill()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill) (_array_): replace all items by the specified one.
- [`pop()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop) (_array_): remove the last item.
- [`push()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) (_array_): append an item to the end.
- [`reverse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse) (_array_): invert the order of the items.
- [`shift()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift) (_array_): remove the first item.
- [`sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) (_array_): change the item order according to the passed function.
- [`splice()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) (_array_): modify the items in varied ways.
- [`unshift()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift) (_array_): prepend an item to the beginning.
- [`append()`]() (_array_): add an item to the end (alias of `push()`).
- [`prepend()`]() (_array_): add an item to the beginning (alias of `unshift()`).
- [`remove()`]() (_array_): remove an item by its index.
- [`assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) (_object_): add new properties as specified in the argument.
- `remove()` (_object_): remove the specified property.
- `extend()` (_object_): add new properties as specified in the passed object (alias of `assign()`).

See them in action:

```js
// For the state of: books = ['a', 'b', 'c']
const { fill, pop, push, ...setBooks } = useActions("books");

fill(1); // [1, 1, 1]
pop(); // ['a', 'b']
push("d"); // ['a', 'b', 'c', 'd']
setBooks.reverse(); // ['c', 'b', 'a']
setBooks.shift(); // ['b', 'c']
setBooks.sort(); // ['a', 'b', 'c']
setBooks.splice(1, 1, "x"); // ['a', 'x', 'c']
setBooks.unshift("x"); // ['x', 'a', 'b', 'c']

// Aliases
setBooks.append("x"); // ['a', 'b', 'c', 'x']
setBooks.prepend("x"); // ['x', 'a', 'b', 'c']
setBooks.remove(1); // ['a', 'c']

// These are immutable, but this still helps:
setBooks.concat("d", "e"); // ['a', 'b', 'c', 'd', 'e']
setBooks.slice(1, 1); // ['b']
setBooks.filter((item) => /^(a|b)$/.test(item)); // ['a', 'b']
setBooks.map((book) => book + "!"); // ['a!', 'b!', 'c!']
setBooks.reduce((all, book) => [...all, book + "x"], []); // ['ax', 'bx', 'cx']
setBooks.reduceRight((all, book) => [...all, book], []); // ['c', 'b', 'a']

// For the state of: user = { id: 1, name: 'John' }
const setUser = useActions("user");
setUser((user) => ({ ...user, name: "Sarah" })); // { id: 1, name: 'Sarah' }

setUser.assign({ name: "Sarah" }); // { id: 1, name: 'Sarah' }
setUser.extend({ name: "Sarah" }); // { id: 1, name: 'Sarah' }
setUser.remove("name"); // { id: 1 }
```

These methods can be extracted right in the actions or used as a method:

```js
const BookForm = () => {
const setBooks = useActions("books");
const onSubmit = (book) => setBooks.append(book);
// OR
const { append } = useActions("books");
const onSubmit = (book) => append(book);

return ...;
};
```

## Examples

Some examples to show how _statux_ works. Feel free to [suggest new ones](https://github.com/franciscop/statux/issues/new?template=suggest-example.md).

### Todo list

A TODO list in 30 lines ([**see codesandbox**](https://codesandbox.io/s/elegant-tdd-c8jlq)):

![TODO List](./assets/todo.jpg "todo example screenshot")

```js
// App.js
export default () => (

TODO List:




);
```

```js
// TodoList.js
import { useStore } from "statux";
import Form from "form-mate";

function TodoItem({ index }) {
const [item, setItem] = useStore(`todo.${index}`);
return (

  • setItem.assign({ done: !item.done })}>
    {item.done ? {item.text} : item.text}

  • );
    }

    export default function TodoList() {
    const [todo, { append }] = useStore("todo");
    return (


      {todo.map((item, i) => (

      ))}



    • Add



    );
    }
    ```

    ### Initial data loading

    Load a pokemon list with graphics from an API ([**see codesandbox**](https://codesandbox.io/s/elastic-glitter-crofz)):

    ![Pokemon List](./assets/pokemon.jpg "pokemon list example screenshot")

    ```js
    // src/App.js
    import Store from "statux";
    import PokemonList from "./PokemonList";

    export default () => (

    The Best 151:




    );
    ```

    ```js
    // src/PokemonList.js
    import { useStore } from "statux";
    import { useEffect } from "react";
    import styled from "styled-components";

    const url = "https://pokeapi.co/api/v2/pokemon/?limit=151";
    const catchAll = () =>
    fetch(url)
    .then((r) => r.json())
    .then((r) => r.results);

    const Pokemon = styled.div`...`;
    const Label = styled.div`...`;

    export default () => {
    const [pokemon, setPokemon] = useStore("pokemon");
    useEffect(() => {
    catchAll().then(setPokemon);
    }, [setPokemon]);
    if (!pokemon.length) return "Loading...";
    return pokemon.map((poke, i) => (

    {poke.name}

    ));
    };
    ```

    ### API calls

    You already saw how to make initial calls on load [in the previous example]().

    Now let's see how to make API calls to respond to a user action, in this case when the user submits the Login form:

    ```js
    // LoginForm.js
    import { useActions } from "statux";
    import axios from "axios";
    import Form from "form-mate";

    export default () => {
    const setUser = useActions("user");
    const onSubmit = async (data) => {
    const { data } = await axios.post("/login", data);
    setUser(data);
    };
    return (



    Login

    );
    };
    ```

    > The libraries [`axios`](https://www.npmjs.com/package/axios) and [`form-mate`](https://www.npmjs.com/package/form-mate) that we are using here are not needed, but they do make our lifes easier.

    ### With localStorage

    Let's say we want to keep all of our small WebApp state in localStorage, we can do that as well:

    ```js
    import Store, { useSelector } from "statux";

    // Define the initial state as an object:
    const todo = JSON.parse(localStorage.todo || "[]");

    // Listen for changes on the state and save it in localStorage:
    const LocalStorage = () => {
    const todo = useSelector("todo");
    localStorage.todo = JSON.stringify(todo);
    return null;
    };

    export default () => (


    ...

    );
    ```

    This can be applied to Dark Mode as well, since localStorage is sync we can read it before running any React to avoid flashing a white screen first:

    ```js
    import Store, { useSelector } from "statux";

    // Define the initial state as an object:
    const dark = localStorage.dark === "true";

    // Save this state fragment when it changes:
    const LocalStorage = () => {
    localStorage.dark = useSelector("dark");
    return null;
    };

    export default () => (


    ...

    );
    ```

    ### Reset initial state

    To reset the initial state we should first keep it separated, and then trigger a reset from the root state ([**see codesandbox**](https://codesandbox.io/s/elastic-haslett-njqjr)):

    ```js
    import Store, { useActions, useStore } from "statux";

    // Define the initial state as an object
    const init = { user: null, todo: [] };

    // We then trigger a useActions without any selector
    const ResetState = () => {
    const setState = useActions();
    const reset = () => setState(init);
    return Clear;
    };

    const Login = () => {
    const [user, setUser] = useStore("user");
    const login = () => setUser("Mike");
    if (user) return

    Hi {user}

    ;
    return (


    Login


    );
    };

    export default () => (




    );
    ```

    ## Motivation

    Why did I create Statux instead of using useState+useContext() or Redux? I built a library that sits between the simple but local React Hooks and the solid but complex full Flux architecture. There are few reasons that you might care about:

    ### React Hooks

    When there's a major shift on a technology it's a good chance to reevaluate our choices. And React Hooks is no different, our components are now cleaner and the code is easier to reuse than ever.

    So I wanted a _minimal_ library that follows React Hooks' pattern of accessing and writing state, but on an app-level instead of a component-level. I tried with Context for a while, but found that you have to create many contexts to avoid some issues ([by design](https://github.com/facebook/react/issues/15156#issuecomment-474590693)) and found that too cumbersome. I just wanted `useState`, but globally.

    So here it is, now you can use `useStore()` as a global `useState()`. I've followed Hooks' syntax where possible, with differences only when needed e.g. not initial state on a component-level since that's global:

    ```js
    const [user, setUser] = useState(null); // React Hooks
    const [user, setUser] = useStore("user"); // Statux
    ```

    ### Direct manipulation

    With Statux you directly define the state you want on your actions. You remove [a full layer of indirection](https://twitter.com/dan_abramov/status/802564042648944642) by **not** following the [Flux architecture](https://www.youtube.com/watch?v=nYkdrAPrdcw).

    This removes a lot of boilerplate commonly seen on apps that use Redux. Where many would define the reducers, actions, action creators, thunk action creators, etc. with Statux you change your state directly:

    ```js
    export default function UserProfile() {
    const [user, setUser] = useStore("user");
    if (!user) {
    const login = () => setUser("Mike");
    return Login;
    }
    return user;
    }
    ```

    This has a disadvantage for very large and complex apps (100+ components) where the coupling of state and actions make changes in the state structure around twice as hard. But if [you are following this Redux antipattern](https://rangle.slides.com/yazanalaboudi/deck) you might not really need Redux, so give Statux a try and it _will_ simplify your code.

    ### Truly immutable

    The whole state is [frozen with `Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) so no accidental mutation can drive subtle bugs and stale state. Try mutating the state of your app for testing ([**see demo**](https://codesandbox.io/s/gallant-firefly-59684)):

    ```js
    const App = () => {
    const [user] = useStore("user");
    // TypeError - can't define property "name"; Object is not extensible
    user.name = "John";
    return

    {user.name}
    ;
    };
    ```

    This will avoid whole categories of bugs for newbies working on your team and experienced devs as well:

    - `arr.sort((a, b) => {...}).map()` is also mutating the original array.
    - `setValue(value++)` will mutate the original value.

    When you try to mutate the state directly it will throw a TypeError. Instead, try defining a new variable if you indeed want to read it with a default:

    ```js
    const App = () => {
    const [user] = useStore("user");
    const name = user.name || "John";
    return

    {name}
    ;
    };
    ```

    Or directly access the name with the correct selector and a default if you know `user` is defined:

    ```js
    const App = () => {
    const [name = "John"] = useStore("user.name");
    return

    {name}
    ;
    };
    ```

    Statux also provides some helpers for modifying the state easily:

    ```js
    // Set the name of the user
    const onClick = (name) => setUser({ ...user, name });
    const onClick = (name) => setUser((user) => ({ ...user, name }));
    const onClick = (name) => setUser.assign({ name });

    // Add a book to the list
    const onSubmit = (book) => setBooks([...books, book]);
    const onSubmit = (book) => setBooks((books) => [...books, book]);
    const onSubmit = (book) => setBooks.append(book);
    ```