About react-use-fp
Motivation
Moderate-to-complex React applications may benefit from the enhanced type-safety of functional libraries like fp-ts, but, while React is reasonably declarative, it mixes a bit awkwardly with some functional concepts. Consider:
const addToLocalStorage =
(storedValue: string): IO<void> =>
() =>
localStorage.setItem('valueToStore', storedValue);
const StorageComponent: React.FunctionComponent<{ valueToBeStored: string }> = ({
valueToBeStored,
}) => {
// we manually invoke addToLocalStorage TWICE to run the IO
const storeOnClick = () => addToLocalStorage(valueToBeStored)();
return <MyCustomButton onClick={storeOnClick} />;
};
This is not inherently bad; the astute reader will have noticed that we can simplify the above by writing the click handler like this:
const storeOnClick = addToLocalStorage(valueToBeStored);
However, for more complicated cases, it would be nice to have some inversion of control. Hence this package. react-use-fp
has three main goals:
- Help keep
React
coding paradigms out of ourfp-ts
code, and vice-versa, in the interest of readability; - Support code correctness by lightly restricting the shape our business logic can take; and
- Help folks exploring functional programming integrate some core FP concepts with React, in a way that feels intuitive.
Usage
react-use-fp
is heavily inspired by Redux middleware. Components interact with the outside world exclusively by dispatching actions. All side effects, computations, and control flow are handled by pure function pipelines composed of fp-ts constructs. From here on, we’ll refer to these pipelines as action handlers. Action handlers also interact with the state exclusively by dispatching actions, and are themselves triggered by dispatched actions.
Actions
Actions are plain JS object actions, exactly like useReducer
/Redux
. However, they must satisfy the following interface to work correctly with react-use-fp
:
interface Action<T> {
type: string;
payload?: T;
}
Additional properties shouldn’t break anything, but also shouldn’t be necessary, and are discouraged.
Action handlers
Similar to Redux action creators, action handlers are pure functions that accept a dispatch function as a parameter and return a function representing the desired computation (IO
or Task
) that react-use-fp
will correctly call at the appropriate time. So, a basic action handler is of type Reader<{dispatch: Dispatch<MyActions>}, IO<void> | Task<void>>
. Here’s an example of a basic action handler:
import {map, tryCatch} from 'fp-ts/TaskEither'
// Notice dispatch is destructured
const fetchDataForState = ({dispatch}) =>
pipe(
tryCatch(
() => fetch('www.someApi.com'),
(e) => `fetch failed for following reason: ${e}`
),
map((data) => dispatch({ type: 'FETCH_SUCCESS', payload: data })),
mapLeft((errInfo) =>
dispatch({ type: 'FETCH_FAILED', payload: errInfo })
)
);
Often, though, we want to pass some initial data for our action handler to operate on. In this case, the action handler’s type would be
(t: PayloadType) => Reader<{dispatch: Dispatch<MyActions>}, IO<void> | Task<void>>
Here’s what the basic action handler above might look like if we updated it to accept a payload to operate on:
//payload action handler
const fetchDataForState = (apiAddress: string) => ({dispatch}) =>
pipe(
tryCatch(
() => fetch(apiAddress),
(e) => `fetch failed for following reason: ${e}`
),
map((data) => dispatch({ type: 'FETCH_SUCCESS', payload: data })),
mapLeft((errInfo) =>
dispatch({ type: 'FETCH_FAILED', payload: errInfo })
)
);
useFPReducer
A curried function that accepts 2 ‘rounds’ of arguments. This is slightly complicated, so let’s break it down. We’ll write some example code just like what we saw above, then see how to plug it into useFPReducer
. As a sidenote, if you’ve worked with redux-toolkit
before, this will look familiar. First, we set up some state:
interface SetCountAction {
type: 'SET_COUNT';
payload: number;
}
interface CountState {
count: number;
}
type CountAction = InitCountAction | SetCountAction;
const countReducer: Reducer<CountState, CountAction> = (state, action) => {
switch (action.type) {
case 'SET_COUNT':
return { count: action.payload };
default:
return { ...state };
}
};
Next, write our handler:
const countHandler =
({dispatch}) =>
() =>
dispatch({ type: 'SET_COUNT', payload: 42 });
And finally, our component. Notice the first argument to useFPReducer
is a plain object with one property. This object’s single key will become the Action
type that triggers our handler function, and the associated property is the function itself. In the second call, we pass in the inital state and reducer we defined above. state
and dispatch
are exactly what you expect them to be–use them as if they were returned from vanilla useReducer
. The thrird item returned is an object populated with typesafe action-creators. Each property on the actions
object is a function that can be called with a payload of the appropriate type, and returns the correct action to initiate the associated handler.
const CountDisplay: React.FunctionComponent<any> = (props) => {
const [state, dispatch, actions] = useFPReducer({UPDATE_COUNT: countHandler})(initialState, countReducer)
const onClick = (e) => dispatch(actions.UPDATE_COUNT());
return <h1 onClick={onClick}>The current count is: {state.count}</h1>;
};
And that’s it! To control the new count instead of always setting it to 42, update the action handler to accept a newCount
parameter…
const countHandler = (newCount: number) =>
({dispatch}) =>
() =>
dispatch({ type: 'SET_COUNT', payload: newCount });
…and add a payload to the action that initiates the action creator
const CountDisplay: React.FunctionComponent<{newCount: number}> = ({newCount}) => {
const [state, dispatch, actions] = useFPReducer({UPDATE_COUNT: countHandler})(initialState, countReducer)
// this time, actions.UPDATE_COUNT should be called with a payload, since the counthandler needs
// input data
const onClick = (e) => dispatch(actions.UPDATE_COUNT(newCount));
return <h1 onClick={onClick}>The current count is: {state.count}</h1>;
};
As long as a payload is dispatched, the hook will correctly invoke the action handler at the appropriate time.
For examples of useFPReducer
with handlers that depend on more than just dispatch, check out the examples section.