Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/franciscop/statux
- Owner: franciscop
- License: mit
- Created: 2019-06-08T11:22:27.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2024-11-10T07:32:09.000Z (2 months ago)
- Last Synced: 2024-12-30T02:13:11.880Z (13 days ago)
- Language: JavaScript
- Homepage: https://statux.dev
- Size: 1.9 MB
- Stars: 71
- Watchers: 4
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-state - statux
- awesome-react-state-management - statux - A straightforward React state management library with hooks and immutable state (List)
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):
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");
returnsetName("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 (
{item.done ? {item.text} : item.text}
);
}
export default function TodoList() {
const [todo, { append }] = useStore("todo");
return (
-
Add
{todo.map((item, i) => (
))}
);
}
```
### 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
};
```
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
};
```
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
};
```
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);
```