Skip to content

Instantly share code, notes, and snippets.

@giladaya
Last active December 23, 2019 15:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save giladaya/7a041728889c89b88ba9213b3f0e3d07 to your computer and use it in GitHub Desktop.
Save giladaya/7a041728889c89b88ba9213b3f0e3d07 to your computer and use it in GitHub Desktop.
Simple hook that allows to "persist" values when navigating between routes, using Redux

DESCRIPTION

Simple custom hook that allows to "persist" values when navigating between routes.
Uses Redux for holding the state so all Redux tools and features are available.

SETUP

Make sure that Redux is setup with the project.
Then, just add the reducer to the global composite reducer:

import { reducer as routeStateReducer } from 'global/hooks/useRouteState';
//...
export default combineReducers({
  //...
  routeState: routeStateReducer,
  //...
});

USAGE

import useRouteState from '.../hooks/useRouteState';
//...
function MyRouteComponent(props) {
  const [someRouteValue, setSomeRouteValue] = useRouteState(
    'MyRouteName',
    'MyValueKey',
    'Default value',
  );
  //...
}
// @flow
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
export const STORE_SEPARATOR = '/';
export const STORE_SLICE = 'routeState';
export const INITIAL_STATE = {};
type Action = { type: String, payload: mixed };
type State = { [string]: mixed };
export function genStateKey(route: string, key: string) {
return `${STORE_SLICE}${STORE_SEPARATOR}${route}${STORE_SEPARATOR}${key}`;
}
export function reducer(state: State = INITIAL_STATE, action: Action): State {
if (action.type.startsWith(STORE_SLICE + STORE_SEPARATOR)) {
const parsedActionType = action.type.split(STORE_SEPARATOR);
const specificAction = parsedActionType[3];
const key = genStateKey(parsedActionType[1], parsedActionType[2]);
switch (specificAction) {
case 'SET':
return { ...state, [key]: action.payload };
case 'REMOVE': {
// eslint-disable-next-line no-unused-vars
const { [key]: _, ...rest } = state;
return rest;
}
default:
return state;
}
}
return state;
}
export default function useRouteState(
route: string,
key: string,
initVal: mixed,
) {
const stateSliceKey = genStateKey(route, key);
const selector = React.useCallback(
state => state[STORE_SLICE][stateSliceKey],
[stateSliceKey],
);
const value = useSelector(selector);
const dispatch = useDispatch();
const set = React.useCallback(
(payload: mixed) =>
dispatch({ type: `${stateSliceKey}${STORE_SEPARATOR}SET`, payload }),
[dispatch, stateSliceKey],
);
const remove = React.useCallback(
() => dispatch({ type: `${stateSliceKey}${STORE_SEPARATOR}REMOVE` }),
[dispatch, stateSliceKey],
);
return [value || initVal, set, remove];
}
import { reducer, genStateKey } from './useRouteState';
describe('genStateKey', () => {
test('Generates correct value', () => {
const expected = 'routeState/recon/tab';
const actual = genStateKey('recon', 'tab');
expect(actual).toEqual(expected);
});
});
describe('reducer', () => {
test('handles SET action correctly', () => {
const expected = {
'routeState/recon/tab': 'foo',
};
const actual = reducer(
{},
{ type: 'routeState/recon/tab/SET', payload: 'foo' },
);
expect(actual).toEqual(expected);
});
test('handles REMOVE action correctly', () => {
const expected = {};
const actual = reducer(
{ 'routeState/recon/tab': 'foo' },
{ type: 'routeState/recon/tab/REMOVE' },
);
expect(actual).toEqual(expected);
});
test('handles unknown action correctly', () => {
const initial = {
'routeState/recon/tab': 'foo',
};
const actual = reducer(initial, {
type: 'routeState/recon/tab/FOO',
payload: 'foo',
});
expect(actual).toEqual(initial);
});
test('handles non routeState action correctly', () => {
const initial = {
'routeState/recon/tab': 'foo',
};
const actual = reducer(initial, { type: 'kokoloko', payload: 'foo' });
expect(actual).toEqual(initial);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment