0.1.1 • Published 5 years ago

noctx v0.1.1

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

noctx

tiny react context/state manager with hooks

Install

npm install --save noctx

Highlights

  • concise api: getCtx(), setCtx(), useCtx(), initValue, check full examples.
  • think in hooks

Example

sandbox github

Basic (setCtx, getCtx)

import React, { useState } from "react";
import { render } from "react-dom";
import noctx from "noctx";

const { setCtx, getCtx } = noctx()

function useCounter(initState = 0) {
  const [counter, setCounter] = useState(initState);
  const increment = () => setCounter(e => e + 1);
  return { counter, increment };
}

const Counter = setCtx("counter", useCounter);

const CounterDisplay = () => {
  const { counter, increment } = getCtx("counter");
  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={increment}>Counter Increment</button>
    </div>
  );
};

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
    </Counter.Provider>
  );
}

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

Full (setCtx, getCtx, useCtx)

./examples/index.js

import React, { useState, useReducer } from 'react';
import { render } from 'react-dom';
import noctx from 'noctx';

const { setCtx, getCtx, useCtx } = noctx()

function useCounter(initValue = 0) {
  const [counter, setCounter] = useState(initValue);
  const increment = () => setCounter(e => e + 1);
  return { counter, increment };
}

// you can use immer.js if preferred
// you can add payload if any
const thirdCounterReducer = (state, action) => {
  switch (action.type) {
    case 'decrement':
      return { ...state, number: state.number - 1 }
    case 'increment':
      return { ...state, number: state.number + 1 }
    default:
      return state
  }
}
function useThirdCounter(initValue) {
  const initState = initValue || { number: 0 }
  const [state, dispatch] = useReducer(thirdCounterReducer, initState);
  return [state, dispatch]
}

const Counter = setCtx('counter', useCounter);
const AnotherCounter = setCtx('anotherCounter', useCounter);
const ThirdCounter = setCtx('thirdCounter', useThirdCounter);

const CounterDisplay = () => {
  const { counter, increment } = getCtx('counter');
  console.log('CounterDisplay')
  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={increment}>Counter Increment</button>
    </div>
  );
};

const AnotherCounterDisplay = () => {
  const { counter, increment } = useCtx(AnotherCounter);
  // if uncomment this line, it will be rerendered if counter in CounterDisplay changes
  // const { counter: counter2, increment: increment2 } = useCtx(Counter);
  console.log('AnotherCounterDisplay')
  return (
    <div>
      <p>Another Counter: {counter}</p>
      <button onClick={increment}>Another Counter Increment</button>
    </div>
  );
};

const ThirdCounterDisplay = () => {
  const [[,dispatch]] = getCtx(['thirdCounter']);
  console.log('ThirdCounterDisplay')
  return (
    <div>
      <button onClick={() => dispatch({ type: 'increment' })}>3rd Counter Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>3rd Counter Decrement</button>
    </div>
  );
};

const AllCounterDisplay = () => {
  const [
    { counter }, 
    { counter: secCounter }, 
    [ThirdCounterState]
  ] = getCtx(['counter', 'anotherCounter', 'thirdCounter']);
  console.log('AllCounterDisplay')
  return (
    <div>
      <p>1st Counter: {counter}</p>
      <p>2nd Counter: {secCounter}</p>
      <p>3rd Counter: {ThirdCounterState.number}</p>
    </div>
  );
};

function App() {
  return (
    <Counter.Provider>
      <AnotherCounter.Provider initValue={2}>
        <ThirdCounter.Provider>
          <div>
            <CounterDisplay />
            <AnotherCounterDisplay />
            <AllCounterDisplay />
            <ThirdCounterDisplay />
          </div>
        </ThirdCounter.Provider>
      </AnotherCounter.Provider>
    </Counter.Provider>
  );
}

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

Best Practice

  1. The component with getCtx('counter') is always updated within the component state changes regardless the context value is changed or not. The reason is here and example is sandbox.

    • if you add some local state changes (e.g. add a setState [toggle, setToggle] inside CounterDisplay, it will be fully rerendered when you setToggle, unless you isolate the logic and use useMemo and memo at the same time, refer to the sandbox above
    • in the full example above, if the button "Counter Increment" is clicked, the CounterDisplay and ThirdCounterDisplay will be updated since they are referring to context 'counter'
    • As of now based on the discussion and practice, the best way to avoid rerendering is to isolate components only with their minimum necessary state / context, so they will not affect each other if non-shared state is changed. Here is the support article: mobx: react-performance
    • if you add getCtx('counter') or useCtx(Counter) in AnotherCounterDisplay but do not use it, after click the button "Counter Increment", all 3 components are still rerendered.
    • in additional, refer to this answer
    • to track the provider value, refer to this
  2. If you prefer to combine the dependency states, you need to refer to the the sandbox above, use useMemo and memo at the same time

  3. There might be some potential solution based on this
  4. Known downside: there is no server-side rendering support as of now.
  5. proposal of useReducer: result is: useReducer will not prevent rerendering, see codes in full example; The reason in this sandbox dispatch does not trigger rerender is: it actaully spilts State.Provider and Dispatch.Provider. This trick may be helpful for those are intersted, but will not be accepted until there is a more efficient way.

Todo

Notable Credits

HIGHLY inspired by unstated-next