Git Product home page Git Product logo

statux's Introduction

Statux npm install statux test badge gzip size

A straightforward React state management library with hooks and immutable state:

Jump to docs for <Store>, useStore(), useSelector(), useActions(), examples.

Getting started

First create a React project (try Create-React-App) and install statux:

npm install statux

Now we are going to initialize our store at the App root level with a couple of initial values:

// src/App.js
import React from 'react';
import Store from 'statux'; // This library
import Website from './Website'; // Your code

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

Finally, use and update these values wherever you want:

// src/User.js
import React from 'react';
import { useStore } from 'statux';

export default () => {
  const [user, setUser] = useStore('user');
  return (
    <div>
      Hello {user ? user.name : (
        <button onClick={e => setUser({ name: 'Maria' })}>Login</button>
      )}
    </div>
  )
};

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): 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): retrieve a specific part of the store state based on the selector or the whole state if none was given.
  • useActions(selector): generate actions to modify the state while avoiding mutations. Includes default actions and can be extended.

<Store>

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

// src/App.js
import Store from 'statux';
import Navigation from './Navigation';

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

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

// src/App.js
import Store from 'statux';
import Navigation from './Navigation';

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

export default () => (
  <Store {...initialState}>
    <Navigation />
  </Store>
);

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 to handle a state subtree. It accepts a string selector and returns an array similar to React's useState():

import { useStore } from 'statux';

export default () => {
  const [user, setUser] = useStore('user');
  return (
    <div onClick={e => setUser({ name: 'Maria' })}>
      {user ? user.name : 'Anonymous'}
    </div>
  );
};

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

import { useStore } from 'statux';

export default () => {
  // If `user` is null, this will throw an error
  const [name = 'Anonymous', setName] = useStore('user.name');
  return (
    <div onClick={e => setName('John')}>
      {name}
    </div>
  );
};

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:

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:

// 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.

useSelector()

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

import { useSelector } from 'statux';

export default () => {
  const user = useSelector('user');
  return <div>{user ? user.name : 'Anonymous'}</div>;
};

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

import { useStore } from 'statux';

export default () => {
  const title = useSelector('books.0.title');
  const name = useSelector('user.name');
  return <div>{title} - by {name}</div>;
};

It accepts both a string selector and a function selector to find the state that we want:

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:

// 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:

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:

import { useActions } from 'statux';
import Form from 'your-form-library';

const ChangeName = () => {
  const setName = useActions('user.name');
  const onSubmit = ({ name }) => setName(name);
  return <Form onSubmit={onSubmit}>...</Form>;
};

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

  • fill() (array): replace all items by the specified one.
  • pop() (array): remove the last item.
  • push() (array): append an item to the end.
  • reverse() (array): invert the order of the items.
  • shift() (array): remove the first item.
  • sort() (array): change the item order according to the passed function.
  • splice() (array): modify the items in varied ways.
  • 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() (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:

// 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:

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

  return <Form onSubmit={onSubmit}>...</Form>;
};

Examples

Some examples to show how statux works. Help me write these? And feel free to suggest new ones.

Todo list

A TODO list in 30 lines (click image for the demo):

TODO List

// App.js
export default () => (
  <Store todo={[]}>
    <h1>TODO List:</h1>
    <TodoList />
  </Store>
);
// TodoList.js
import { useStore } from "statux";
import React from "react";
import forn from "forn";

const Todo = ({ index }) => {
  const [item, setItem] = useStore(`todo.${index}`);
  return (
    <li onClick={() => setItem.assign({ done: !item.done })}>
      {item.done ? <strike>{item.text}</strike> : item.text}
    </li>
  );
};

export default () => {
  const [todo, { append }] = useStore("todo");
  return (
    <ul>
      {todo.map((item, i) => (
        <Todo key={i + "-" + item.text} index={i} />
      ))}
      <li>
        <form onSubmit={forn(append, { reset: true })}>
          <input name="text" placeholder="Add item" />
          <button>Add</button>
        </form>
      </li>
    </ul>
  );
};

Initial data loading

See pokemon loading list with graphics (click image for the demo):

Pokemon List

// src/App.js
import Store from 'statux';
import React from 'react';
import PokemonList from './PokemonList';

export default () => (
  <Store pokemon={[]}>
    <h1>The Best 151:</h1>
    <PokemonList />
  </Store>
);
// src/PokemonList.js
import { useStore } from "statux";
import React, { 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 = ({ id, children }) => <li id={id}>{children}</li>;

export default () => {
  const [pokemon, setPokemon] = useStore("pokemon");
  useEffect(() => {
    catchAll().then(setPokemon);
  }, []);
  if (!pokemon.length) return "Loading...";
  return (
    <ul>
      {pokemon.map((poke, i) => (
        <li key={i} id={i + 1}>
          <Label>{poke.name}</Label>
        </li>
      ))}
    </ul>
  );
};

API calls

When calling an API, make sure you are using React's useEffect():

// Login.js
export default () => {
  const [auth, setAuth] = useStore('auth');
  const onSubmit = useCallback(async data => {
    const token = await api.login(data);
    setAuth(token);
  }, [auth]);
  return (
    <LoginForm onSubmit={onSubmit}>
      {...}
    </LoginForm>
  );
}

Login and localStorage

Now that you know how to call an API for long, let's see how to store/retrieve the token in localStorage automatically:

import Store, { useSelector } from 'statux';

// Initial auth token load
const auth = localStorage.getItem('auth');

// Save/remove the auth token when it changes anywhere in the app
const TokenUpdate = () => {
  const auth = useSelector('auth');
  useEffect(() => {
    localStorage.setItem('auth', auth);
  }, [auth]);
  return null;
};

export default () => (
  <Store auth={auth}>
    <TokenUpdate />
    ...
  </Store>
)

Reset initial state

Motivation

Why did I create this instead of using useState+useContext() or Redux? There are few reasons that you might care about:

Direct manipulation

It is a lot simpler in the way it handles state, which is great to avoid the relatively huge boilerplate that comes with small projects with Redux. Instead of defining the reducers, actions, action creators, thunk action creators, etc. you manipulate the state directly. Statux removes a full layer of indirection.

On the downside, this couples the state structure and operations, so for large projects something following the Flux architecture like Redux would be better suited. If you are following this Redux antippatern you can give Statux a try.

Truly immutable

The whole state is frozen with Object.freeze() so no accidental mutation can drive subtle bugs and stale state. Try mutating the state of your app for testing (see demo):

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

This will avoid whole categories of bugs. Did you know these for instance?

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

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

const App = () => {
  const [user] = useStore("user");
  const name = user.name || 'John';
  return <div>{name}</div>;
};

Or directly access the name with the correct selector and a default:

const App = () => {
  const [name = 'John'] = useStore("user.name");
  return <div>{name}</div>;
};

When you want to change the state, you can do it without mutations or use one of the helpers we provide:

// 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);

statux's People

Contributors

franciscop avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.