1.4.11 • Published 3 years ago

react-hooksack v1.4.11

Weekly downloads
36
License
MIT
Repository
github
Last release
3 years ago

npm version npm bundle size (scoped) Codecov unit-tests npm-publish

React HookSack

A lightweight, fully typed store for react, based entirely on hooks.

Keep re-rendering of involved components at a minimum. See this codesandbox with nested components. (thanks to @andrewgreenh for creating #3)

Edit 487k2wzpq4

Install

npm install --save react-hooksack
# or
yarn add react-hooksack

Usage

Import

import makeStore from 'react-hooksack';

initialize new store (hook)

const useStore = makeStore(initialValue, reducer);
objectrequiredtypedescription
initialValueyesStateWhatever you pass as initial value becomes the store`s state type (we will refer asState).makeStoreis type generic, so you could set more complex types like this:makeStore<null \| number>(null)
reducerno(  state: State,  action: ReducerAction) => StateIf passed, setState will only accept ReducerAction later.

use within components

// get state and setter
const [state, setState] = useStore();

// or just get state
const state = useStore('justState');

// or just get setter (this one avoid re-renders)
const setState = useStore('justSetter');
objecttyperequireddescription
useStore(  just?: 'justState' | 'justSetter') =>  | State, setState  | State  | setStateWithout arguments, useStore behaves like useState of React (except for the initial value). Otherwise you get what you asked for (see below).
stateStateWill be of the type you set as initialValue on makeStore. Every component that consumes state will re-render whenever state changes.
setState (no reducer)(  argument:    | State    | ((currentState: State) => State)) => voidHooksack is using React`s useState hook under the hood. Thus, you can just pass the new state of type State or pass a function that gets the current state and has to return the new state.
setState (reducer)(  action: ReducerAction) => voidIf makeStore was initialized with a reducer, setState expects that reducer`s ReducerAction as argument.
just'justState'nouseStore will just return the current state of type State.
just'justSetter'nouseStore will just return the state setter setState depending on if you passed a reducer to makeStore or not (see above). Components that just get the state setter through literal 'justSetter' will not get re-rendered whenever state changes.

Example

use without reducer

Edit 487k2wzpq4

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import makeStore from 'react-hooksack';

// util for counting rerenders
import { LogTable, useLogStore } from './logRender';

// make a new store and set it's initial value (to 0)
const useClickStore = makeStore(0);

// a component that subscribes to the current state of "clicks"
// und uses the setter "setClicks"
const ViewAndUpdate = () => {
  // by consuming the state "clicks" this component will rerender
  // every time, "clicks" gets updated
  const [clicks, setClicks] = useClickStore();

  // just to count rerenderings
  const logRender = useLogStore('justSetter');
  React.useEffect(() => logRender('ViewAndUpdate'));

  return (
    <button
      title="ViewAndUpdate"
      onClick={() => setClicks(currentClicks => currentClicks + 1)}
    >
      add click (currently {clicks})
    </button>
  );
};

// a simple component to view the store's state
const ViewOnly = () => {
  // just subsribing to the state of "clicks" - no setter required here
  // this component will rerender with every update of "clicks" too
  const clicks = useClickStore('justState');

  // just to count rerenderings
  const logRender = useLogStore('justSetter');
  React.useEffect(() => logRender('ViewOnly'));

  return (
    <div title="ViewOnly">
      <span>clicks: {clicks}</span>
    </div>
  );
};

// a component that will only set the new state for "clicks"
const UpdateOnly = () => {
  // by just using the setter for "clicks" this component will not
  // rerender every time "clicks" updates
  const setClicks = useClickStore('justSetter');

  // just to count rerenderings
  const logRender = useLogStore('justSetter');
  React.useEffect(() => logRender('UpdateOnly'));

  return (
    <button title="UpdateOnly" onClick={() => setClicks(clicks => clicks + 1)}>
      add click
    </button>
  );
};

const App = () => (
  <div>
    <ViewOnly />
    <ViewAndUpdate />
    <UpdateOnly />
    <hr />
    <LogTable />
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));

use with reducer

Edit rmj4vyyn04

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import makeStore from 'react-hooksack';

// shape a ToDo
interface Todo {
  id: number;
  name: string;
  done: boolean;
}

// define the ToDo store reducer
const reducer = (
  state: Todo[],
  action: { type: 'add' | 'del' | 'toggle'; todo: Todo },
) => {
  switch (action.type) {
    case 'add':
      // ensure to always return a new object
      return [...state, action.todo];

    case 'del':
      // ensure to always return a new object
      return state.filter(todo => todo !== action.todo);

    case 'toggle':
      // ensure to always return a new object
      return state.map(todo =>
        todo === action.todo ? { ...todo, done: !todo.done } : todo,
      );

    default:
      throw new Error();
  }
};

// make a new store, set it's initial value and pass reducer
const useTodoStore = makeStore(
  [
    { id: 1, done: false, name: 'remember the milk' },
    { id: 2, done: false, name: 'feed the cat' },
    { id: 3, done: true, name: 'walk the dog' },
    { id: 4, done: true, name: "order a table at Luigi's" },
  ],
  reducer,
);

// component to render and toggle a Todo
const Todo: React.FC<{ todo: Todo }> = ({ todo }) => {
  const setTodos = useTodoStore('justSetter');
  const style = {
    textDecoration: todo.done ? 'line-through' : undefined,
  };
  const handleChange = () => {
    setTodos({ type: 'toggle', todo });
  };

  return (
    <li>
      <input type="checkbox" checked={todo.done} onChange={handleChange} />
      <span style={style}>{todo.name}</span>
    </li>
  );
};

// component to render a list of Todos
const TodoList: React.FC<{ todos: Todo[] }> = ({ todos }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} todo={todo} />
    ))}
  </ul>
);

// component to tell appart already done Todos from to be tone ones
const AllTodos = () => {
  const todos = useTodoStore('justState');
  const toBeDone = todos.filter(todo => todo.done === false);
  const done = todos.filter(todo => todo.done);

  return (
    <div>
      {toBeDone.length > 0 && (
        <div>
          <h1>to be done</h1>
          <TodoList todos={toBeDone} />
        </div>
      )}
      {done.length > 0 && (
        <div>
          <h1>already done</h1>
          <TodoList todos={done} />
        </div>
      )}
    </div>
  );
};

// component to add a new, undone todo
const AddTodo = () => {
  const dispatchTodos = useTodoStore('justSetter');
  const todoInput = React.useRef<HTMLInputElement>(null);
  const addTodo = () => {
    if (todoInput && todoInput.current) {
      const name = todoInput.current.value;
      dispatchTodos({
        type: 'add',
        todo: { name, done: false, id: Date.now() },
      });
      todoInput.current.value = '';
    }
  };

  return (
    <div>
      <input ref={todoInput} placeholder="new Todo name" />
      <button onClick={addTodo}>add</button>
    </div>
  );
};

const App = () => (
  <div>
    <AllTodos />
    <AddTodo />
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));

Tests

npm run test
# or
npm run test:coverage

Why

I got inspired by a blog post of Jhonny Michel. He also released react-hookstore but I:

  • don't like to register a new store with a string passed
  • prefer functions over classes
  • like Typescript / type support
1.4.11

3 years ago

1.4.10

4 years ago

1.4.9

4 years ago

1.4.8

4 years ago

1.4.6

4 years ago

1.4.5

4 years ago

1.4.7

4 years ago

1.4.3

4 years ago

1.4.2

4 years ago

1.4.1

4 years ago

1.4.0

4 years ago

1.3.9

4 years ago

1.3.8

4 years ago

1.3.7

4 years ago

1.3.6

5 years ago

1.3.5

5 years ago

1.3.4

5 years ago

1.3.3

5 years ago

1.3.2

5 years ago

1.3.1

5 years ago

1.3.0

5 years ago

1.2.0

5 years ago

1.1.2

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.0

5 years ago