Functional React State Management with FunState and TypeScript
Tags: typescript, javascript, react
Published: 2021-07-23 20:47:54

React 16.8 gave us hooks, a concise way of organizing our components and separating complexity from our functional components. With hooks like useState we could consider eschewing state management solutions entirely. However, trying to useState on medium to large applications has quite a few challenges:

* Using many useState calls bloat components and causes an explosion of variables to manage as each call creates value and setter functions. This in-turn bloats child components as you have to add properties for all the related values and setters.
* Code with useState can be difficult to write unit tests for.
* It can be hard to refactor logic out of complex components(essentially requires custom hooks which are themselves hard to unit test.)
* No convenient way of dealing with immutable nested data (other than the JS spread operator)
* useReducer adds its own complexity and while simpler than redux, it introduces actions and reducers which then have to be managed in their own ways.
* Making useState enclose a complex state object can solve some of the problems but makes it harder to write child components that are only operating on a subset of the bigger state nodes.

# Another State-Management Library Appears!

[FunState](https://github.com/fun-land/fun-land/tree/main/packages/use-fun-state) is a new state-management solution that leverages the convenience of useState with an api that allows for fractal, testable, and composable components.

# Refactoring to Fun

Let's start with a small component using vanilla React to show how you'd convert to using fun-state:

```typescript
export const Counter: FC<{
  value: number,
  onChange: (x: number) => unknown
} = (props) => {
  const onCountChange: ChangeEventHandler<HTMLInputElement> = (e) => {
    const val = parseInt(e.currentTarget.value, 10);
    if (isFinite(val)) {
      props.onChange(val);
    }
  };
  const onUp = () => props.onChange(inc);
  const onDown = () => props.onChange(dec);
  return (
    <div>
      <input value={value} onChange={onCountChange} />
      <button onClick={onUp}>up</button>
      <button onClick={onDown}>down</button>
    </div>
  );
};

// Usage in an App
const App: FC = () => {
  const [counterValue, setCounterValue] = useState(0);
  return (
    <div>
      <Counter
        value={counterValue}
        onChange={setCounterValue} />
    </div>
  );
};
```
Here we can swap out `useState` for `useFunState`
```typescript
import {FC, useState} from 'react';
import useFunState from '@fun-land/use-fun-state';
import {FunState} from '@fun-land/fun-state';

export const Counter: FC<{state: FunState<number>>}> = ({state}) => {
  const value = state.get();
  const onCountChange: ChangeEventHandler<HTMLInputElement> = (e) => {
    const val = parseInt(e.currentTarget.value, 10);
    if (isFinite(val)) state.set(val);
  };
  const onUp = () => state.mod(inc);
  const onDown = () => state.mod(dec);
  return (
    <div>
      <input value={value} onChange={onCountChange} />
      <button onClick={onUp}>up</button>
      <button onClick={onDown}>down</button>
    </div>
  );
};

const App: FC = () => {
  const counterState = useFunState(0);
  return (
    <div>
      <Counter
        state={counterState} />
    </div>
  );
};
```
You may reasonably be thinking, "How is this better?" Let's explore how this code changes over time. 

What if we want to have an array of counters?

Thankfully we don't have to change the implementation of Counter in either approach.

Vanilla:
```typescript
const App: FC = () => {
  const [counters, setCounter] = useState([0, 1, 2, 3, 4]);
  return (
    <div>
      {counters.map((counter, i) => (
        <Counter
          value={counter}
          onChange={(val) => setCounter( counters.map((c, j) => i === j ? val : c))} />
    </div>
  );
};
```

FunState
```typescript
import {index} from '@fun-land/accessor';

const App: FC = () => {
  const countersState = useFunState([0, 1, 2, 3, 4]);
  return (
    <div>
      {countersState.get().map((_, i) => (
        <Counter state={countersState.focus(index(i))} />
      )}
    </div>
  );
};
```
The magic here is that since `Counter` expects a `FunState<number>` instance, we just need to focus on one. `index` is an Accessor that can point to a specific item in an array, so no custom state handling required. We're just connecting wires.

# Unit Testing

One of the useful properties of components using FunState is that since the state is first-class it can be passed in. FunState also provides a library-agnostic FunState constructor, `mockState`, to ease unit testing.

```typescript
import {render, fireEvent} from '@testing-library/react';
import {mockState} from '@fun-land/fun-state'

describe('Counter', () => {
  it('increments state when up button pressed', () => {
    const counterState = mockState(0);
    const comp = render(<Counter state={counterState} />);
    fireEvent.click(comp.getByText('up'));
    expect(counterState.get()).toBe(1);
  });
});
```
No magic mocks or spies required!

Another neat trick is to extract functions from the body of your components to keep cyclomatic complexity under control.

For example let's extract `onCountChange`:
```typescript

const onCountChange = (state: FunState<number>): ChangeEventHandler<HTMLInputElement> = (e) => {
    const val = parseInt(e.currentTarget.value, 10);
    if (isFinite(val)) state.set(val);
  };
```
Then in the component you can just partially apply the state:

```typescript
...
<input value={value} onChange={onCountChange(state)} />
```
Then you can test the handler directly if you like:
```typescript
describe('onCountChange', () => {
  it('updates the state if a valid integer is passed', () => {
    const counterState = mockState(0);
    onCountChange(counterState)({currentTarget: {value: 12}} as ChangeEvent)
    expect(counterState.get()).toEqual(12);
  });
});
```

# What's great about FunState

* Rather than adding indirection of actions and reducers just set the state in event handlers without shame
* Focus into the state and pass subsets of it to functions or child components.
* Write unit tests easily with provided `mockState`.
* Good type-safety with typescript so the compiler can ensure that everything is copacetic
* First-class state makes refactoring easier.
* Integrate into existing React 16.8+ application without having to change anything else.
* Also works with React Native
* Tree-shakable so you only bundle what you use.

This is just the tip of the iceberg and I plan to go more in-depth on future articles. Give a ❤️ if you'd like to see more!