Immer + Hooks + Context + TypeScript = Low boilerplate, Immutable, Editor-friendly State management?
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src
test
.gitignore
.npmignore
LICENSE
README.md
jest.config.js
package-lock.json
package.json
tsconfig.json

README.md

immutable-context

Experiment in state management

npm i -s immutable-context

Immer + Hooks + Context + TypeScript = Low boilerplate, Immutable, Editor-friendly State management?

https://www.npmjs.com/package/immutable-context

What/why/how?

You shouldn't use this on anything important... but here's how it should work:

type CounterType = { count: number };

const { StateProvider, useImmutableContext } = createImmutableContext<
  CounterType
>({ count: 0 });

const Counter = () => {
  const { apply, state } = useImmutableContext();
  const increment = () =>
    apply(s => {
      s.count++;
    });
  return <button onClick={increment}>Count: {state.count}</button>;
};

const App = () => (
  <StateProvider>
    <Counter />
  </StateProvider>
);

Longer example/step-by-step:

1. Define a type for your state:

type ExampleType = {
  count: number;
  deeply: {
    nested: {
      thing: {
        like: number;
      };
    };
  };
};

2. use createImmutableContext to generate a provider and hook for use in components

History included here to demonstrate immutability. Your editor should autocomplete stuff nicely.

const history: ExampleType[] = [];
const { StateProvider, useImmutableContext } = createImmutableContext<
  ExampleType
>(
  {
    count: 0,
    deeply: {
      nested: {
        thing: {
          like: 5
        }
      }
    }
  },
  options: {
    onUpdate: s => {
      history.push(s);
      console.log(history);
    }
  }
);

3. Use the hook

  • dispatch takes a function that mutates the state. Execpt it uses immer do doesn't really mutate the state
  • state is the state
const CountThing = () => {
  const { apply, state } = useImmutableContext();

  return (
    <div>
      <p>{state.count}</p>
      <button
        onClick={() =>
          apply(s => {
            s.count++;
          })
        }
      >
        Hit me
      </button>
      <p>{state.deeply.nested.thing.like}</p>
    </div>
  );
};

Another example component (this time not actually using state so no need to destructure):

const DeepDiveUpdate = () => {
  const { apply } = useImmutableContext();

  return (
    <div>
      <button
        onClick={() =>
          apply(s => {
            s.deeply.nested.thing.like--;
            s.count++;
          })
        }
      >
        Dive!
      </button>
    </div>
  );
};

4. Put together in an app

Yeah, I totally ignored how you'll really need to provide the useImmutableContext to components, but you would need to pass around/inject the Context with useContext anyway.

class App extends Component {
  render() {
    return (
      <StateProvider>
        <CountThing />
        <CountThing />
        <DeepDiveUpdate />
      </StateProvider>
    );
  }
}

This example in a create-react-app project.

PS

In a real application (ha!) would do something more like, probably in entirely different file:

const multiUpdate = apply => () =>
  apply(s => {
    s.deeply.nested.thing.like--;
    s.count++;
  });

Yes, just vanilla JS, very testable (and deletable). Then the component becomes:

const DeepDiveUpdate = () => {
  const { apply } = useImmutableContext();
  const onUpdate = multiUpdate(apply);
  return (
    <div>
      <button onClick={onUpdate}>Dive!</button>
    </div>
  );
};

For async stuff:

const asyncMultiUpdate = apply => async () => {
  apply(s => {
    s.count++;
  });
  await longRunningThing();
  apply(s => {
    s.deeply.nested.thing.like--;
  }
}