react-stateful-component

2.0.2 • Public • Published

React Stateful Component

React Stateful Component provides tools to create stateful React components using just functions.

Features:

  • Uses a reducer to manage state
  • The reducer can schedule side effects following the same pattern as Elm and Reason-React
  • Side effects are run outside of the component, meaning you can test your components without having to execute side effects
  • life cycle hooks
  • Subscriptions to handle communication with the "outside world"
  • Static type checking with Flow

> TodoMVC example

Getting started

Install React Stateful Component using npm:

npm i react-stateful-component --save

Import React Stateful Component into your project:

import createComponent, { update } from 'react-stateful-component';

Next, write your component:

const add = () => ({ type: 'ADD' });
const subtract = () => ({ type: 'SUBTRACT' });
 
const Counter = createComponent(() => ({
    initialState: () => ({
        counter: 0
    }),
    reducer: (state, action) => {
        const { counter } = state;
 
        switch (action.type) {
            case 'ADD':
                return update.state({ counter: counter + 1 });
            case 'SUBTRACT':
                return update.state({ counter: counter - 1 });
            default:
                return update.nothing();
        }
    },
    render: ({ state, reduce }) => (
        <div>
            <button onClick={() => reduce(add())}>+</button>
            <span>{state.counter}</span>
            <button onClick={() => reduce(subtract())}>-</button>
        </div>
    )
}));

Wrap the component in a SideEffectProvider in order to use it:

import { SideEffectProvider } from 'react-stateful-component';
import Counter from './counter';
 
ReactDOM.render(
    <SideEffectProvider>
        <Counter />
    </SideEffectProvider>,
    document.getElementById('app')
);

Creating a component

Component definition

A component definition is a function that returns an object describing your component. The most basic component definition would look something like this:

const myComponentDefinition = () => ({
    initialState: () => ({ counter: 0 }),
    reducer: state => update.nothing(),
    render: ({ state }) => <div>{state.counter}</div>
});

A component definition should define at least an initialState, reducer, and render function. At first sight this might look pretty similar to defining a class based component. There is an important difference though. All of these functions can be run in isolation because they can not use this and their output is based on their input parameters.

Once you have your component definition, you can use the createComponent function to actually create the Component.

import createComponent, { update, SideEffectProvider } from 'react-stateful-component';
 
const myComponentDefinition = () => ({
    initialState: () => ({ counter: 0 }),
    reducer: state => update.nothing(),
    render: ({ state }) => <div>{state.counter}</div>
});
 
const MyComponent = createComponent(myComponentDefinition);
 
ReactDOM.render(
    <SideEffectProvider>
        <MyComponent />
    </SideEffectProvider>,
    document.getElementById('app')
);

Managing state with a reducer

React Stateful Component uses a reducer to manage the component's state. Since all state updates happen in one place, it'll be easier to understand the state of the component, compared to having setState calls spread across multiple methods.

Because the reducer is just a function, it can be extracted and unit tested separately. For example you could put your reducer into a separate file, if your component has a lot of state interactions, that might be a good approach. With smaller components you might want to keep the reducer in the same file but export it separately.

import { update } from 'react-stateful-component';
 
export const myReducer = (state, action) => {
    switch (action.type) {
        case 'ADD':
            return update.state({ counter: state.counter + 1 });
        case 'SUBTRACT':
            return update.state({ counter: state.counter - 1 });
        default:
            return update.nothing();
    }
};
 
const myComponentDefinition = () => ({
    initialState: () => ({ counter: 0 }),
    reducer,
    render: ({ state }) => <div>{state.counter}</div>
});
 
export default createComponent(myComponentDefinition);

Just like in Redux the reducer works with State and Actions. However, you might have noticed a difference between a Redux reducer and the reducers used in the examples above. The reducers in these examples aren't just returning state. Instead they are returning an Update<S, A>. The next section will explain these updates in more detail.

Update types

Since the reducer is not only responsible for updating the state but can also schedule side effects, only returning the state from the reducer wouldn't be really useful. Instead we will return an Update<S, A>. You should look at an Update as an instruction for the component. It can either update the state, schedule a side effect, do both or instruct the component to just do nothing.

Example:

import { update } from 'react-stateful-component';
 
const myReducer = (state, action) => {
    switch (action.type) {
        case 'ADD':
            return update.state({ counter: state.counter + 1 });
        case 'SUBTRACT':
            return update.state({ counter: state.counter - 1 });
        default:
            return update.nothing();
    }
};

update.state(state)

<S>(state: S) => UpdateState<S>

update.sideEffect(sideEffect)

<A, S>(sideEffect: SideEffect<A, S>) => UpdateSideEffect<A, S>

update.stateAndSideEffect(state, sideEffect)

<S, A>(state: S, sideEffect: SideEffect<A, S>) => UpdateStateAndSideEffect<S, A>

update.nothing()

() => UpdateNothing

Render

The render function that is part of the definition works almost the same as a stateless React component. It will receive an object with the properties reduce, state and props as an input parameter.

The type signature of the render function:

<S, P, A>(me: {reduce: Reduce<A>, state: S, props: P}) => React.Node

Rendering state and properties

const render = ({ state, props }) => (
    <div>
        <div>{props.name}</div>
        <div>{state.counter}</div>
    </div>
);

Triggering state changes

To trigger a state change you will need to call reduce() with an action, this will cause the components reducer to be invoked with the specified action. Your reducer can then calculate the new state which will cause the component the re-render with the new state.

const render = ({ state, reduce }) => (
    <div>
        <button onClick={() => reduce({ type: 'ADD' })} />
        <div>{state.counter}</div>
        <button onClick={() => reduce({ type: 'SUBTRACT' })} />
    </div>
);

Working with side effects

In order to keep a component clean and testable we try to push everything that isn't directly related to reducing actions outside of the component. This can be done by using side effects.

Side effects are functions that have access to reduce(). This means they can reduce actions and by doing so trigger state changes within the component.

For example, a side effect can be used for async tasks like fetching data from an api or to start timers, but also to read from or write to localStorage.

The type signature of a side effect looks like this:

type SideEffect<A, S> = (reduce: Reduce<A>, state: S) => any;

All side effects are executed outside of the component, a reducer will only schedule a side effect, it will not execute it. This enables us to unit test component without having to worry about side effects.

Example of a side effect function:

const fetchUsersReceived = users => ({
    type: 'FETCH_USERS_RECEIVED',
    users
});
 
const fetchUserFailed = err => ({
    type: 'FETCH_USERS_FAILED',
    err
});
 
const mySideEffect = reduce =>
    fetch('http://myapp.com/api/users')
        .then(user => {
            reduce(fetchUsersReceived(users));
            return users;
        })
        .catch(err => {
            reduce(fetchUserFailed(err));
        });

Side effects can be scheduled from within the reducer using either update.sideEffect(sideEffect) or update.stateAndSideEffect(state, sideEffect). The first update type will only schedule a side effect, while you can use the second one to both update the state and then schedule a side effect.

Example of a reducer scheduling a side effect:

const myReducer = (state, action) => {
    switch (action.type) {
        case 'FETCH_USERS':
            return update.stateAndSideEffect(
                { ...state, isPending: true },
                fetchUsers // Note that we only pass the sideEffect, and not execute it here
            );
        case 'FETCH_USERS_RECEIVED':
            return update.state({
                ...state,
                isPending: false,
                users: action.users
            });
        default:
            return update.nothing();
    }
};

Subscriptions

Sometimes your component will need to interact with the outside world. For example by subscribing to events on a global event bus, by listening to click events that happen outside of your component or by starting a timer.

A subscription is a function that gets reduce and refs as parameters and returns a function. The function that is returned from the subscription is used to release the subscription.

Subscriptions will be automatically initialised in didMount and they will be released in willUnmount.

Subscriptions are initialised and released outside of the Component but within the SideEffectProvider, meaning you can ignore or mock them while unit testing.

Example:

const intervalSubscription = (reduce, refs) => {
    const interval = setInterval(() => reduce({ type: 'TICK' }), 1000);
 
    const releaseSubscription = () => clearInterval(interval);
 
    return releaseSubscription;
};
 
const myComponentDefinition = () => ({
    initialState: () => ({ counter: 0 }),
    subscriptions: [intervalSubscription],
    reducer: (state, action) => {
        switch (action.type) {
            case 'TICK':
                return update.state({ counter: state.counter + 1 });
            default:
                return update.nothing();
        }
    },
    render: ({ state }) => <div>{state.counter}</div>
});

Refs

When you need to interact with a DOM element directly you will need to use refs. the refs property is part of the Me object that is sent into render. From render you can assign a ref just like you would do in a regular React component, the only difference is that you would assign it to the refs object instead of to the class instance.

Refs can be accessed from sideEffects and from subscriptions.

Example:

const focusInput = (reduce, state, refs) => {
    if (!refs.input) return;
    refs.input.focus();
};
 
const myComponentDefinition = () => ({
    initialState: () => ({ counter: 0 }),
    reducer: (state, action) => {
        switch (action.type) {
            case 'INIT':
                return update.sideEffect(focusInput);
            default:
                return update.nothing();
        }
    },
    didMount: ({ reduce }) => {
        reduce({ type: 'INIT' });
    },
    render: ({ state, refs }) => (
        <div>
            <input ref={ref => (refs.input = ref)} type="text" />
        </div>
    )
});

API

A component definition has a required and optional properties. initialState, reducer and render are required. Just like class based components a definition can specify certain lifeCycle hooks. All standard React lifecycle hooks are available except for willMount. Please note lifecycle hooks are not prefixed with "component", so instead of componentDidMount you can just use didMount.

Me

Almost all of the functions that are part of the definition (except for initialState and the reducer) will receive object of the type Me<P, S, A> as parameter. This object contains data and functions to work with the component. It contains the state, props, vars and the reduce function.

type Me<P, S, A> = {
    state: S,
    props: P,
    reduce: Reduce<A>,
    refs: Refs
};

Refs

type Refs = {
    [key: string]: ?HTMLElement
};

A component definition can have the following properties defined:

initialState

<S, P>(props: P) => S

subscriptions

Array<Subscription<A>>

reducer

<S, A>(state: S, action: A) => Update<S, A>

render

<S, P, A, V>(me: Me<P, S, A>) => React.Node

displayName (optional)

string

didMount (optional)

<S, P, A, V>(me: Me<P, S, A>) => void

willUnmount (optional)

<S, P, A, V>(me: Me<P, S, A>) => void

didUpdate (optional)

<S, P, A, V>(prevMe: { state: S, props: P }, me: Me<P, S, A>) => void

shouldUpdate (optional)

<S, P, A, V>(nextMe: { state: S, props: P }, me: Me<P, S, A>) => boolean

Package Sidebar

Install

npm i react-stateful-component

Weekly Downloads

2

Version

2.0.2

License

MIT

Unpacked Size

33.8 kB

Total Files

14

Last publish

Collaborators

  • vejersele