With React hooks, these utilities are not needed and the package is not maintained anymore.
Only leftover is routing-history which was moved to @ackee/redux-utils.
Chris
Useful additions for your redux - react-router based app.
Name of package refers to Saint Christopher, patron of travellers at their routes. Chris is just its shorter form used for easier remembrance and writability.
Table of contents
Installation
Using npm:
npm i -s @ackee/chris
Using yarn:
yarn add @ackee/chris
API
Selectors
routingSelector
Selector for connected-react-router
state, which returns location
.
Sagas
runRouteDependencies(handlers: {[ActionType], sagaHandler}, selector: ReduxSelector)
With usage of runRouteDependencies
and routing-history
, you can exclude business logic from React componentDidMount
and componentWillUnmount
and download the data you need for the current page outside of React components.
Example - react to new route
import { runRouteDependencies } from '@ackee/chris';
const handlers = {
'/user/:id': function* ({ id }) {
// fetch user data and store it
}
};
export default function* () {
yield all([
takeEvery(LOCATION_CHANGE, runRouteDependencies, handlers),
])
}
Example - react to new route and clean data when leaving it
import { runRouteDependencies } from '@ackee/chris';
import routingHistory from '@ackee/chris/routing-history';
const { activeLocationSelectorFactory, previousLocationSelectorFactory } = routingHistory;
// 'history' is the name of routingHistory module state
const activeLocationSelector = activeLocationSelectorFactory('history');
const previousLocationSelector = previousLocationSelectorFactory('history');
const handlers = {
'/user/:id': function* ({ id }) {
// fetch user data and store it
}
};
const postHandlers = {
'/user/:id': function* ({ id }) {
// flush user data from the store
}
};
export default function* () {
yield all([
takeEvery(LOCATION_CHANGE, runRouteDependencies, handlers, activeLocationSelector),
takeEvery(LOCATION_CHANGE, runRouteDependencies, postHandlers, previousLocationSelector),
])
}
runRouteActions(handlers: {[ActionType], sagaHandler})
Alias for runRouteDependencies
saga.
routeRefresh(initType: ActionType, type: ActionType, handlers: function)
Saga to refresh route dependecies. It run runRouteDependencies(handlers)
every time initType
action is dispatched.
Also action with type
is dispatched before runRouteDependencies
is invoked.
Let's say we have an application with more pages that have list of items with paging. Every time page change we want to load new data for particular page.
export default function* () {
yield all([
routeRefresh(
actionTypes.app.SET_PAGE_START,
actionTypes.app.SET_PAGE,
{
'/users': getUsers,
'/invoices': getInvoices,
...
},
),
])
}
When user change page of viewed list in application action SET_PAGE_START
instead of SET_PAGE
should be dispatched.
Then SET_PAGE_START
is caught in routeRefresh
and following happen
- action
SET_PAGE
is dispatched with all properties (except thetype
of course) fromSET_PAGE_START
. TheSET_PAGE
action should set the page to the state. - as a next step, route dependencies are procceded with this call
runRouteDependencies({ '/users': getUsers, '/invoices': getInvoices, ... })
runSagas(sagas: {[ActionType]: sagaHandler})
Automatically invokes all given sagas for given event. The invoke of saga is wrapped with try-catch saga that dispatch relevant actions. Look at the example fo better undestanding
function*() getData {
// This is a pseudo code, implementation of getting data from the API
// and setting them back to the state is up to you
const users = yield api.get(config.api.users);
yield put(setUsers(users));
}
export default function*() {
return yield runSagas({
[actionTypes.designs.REQUEST_USERS]: getData,
[actionTypes.designs.REQUEST_OTHER_DATA]: getOtherData,
...
});
}
- Once
REQUEST_USERS
is dispatched in application it's caught and handled bygetData
handler. - When
getData
saga- throw an error during its run, action
REQUEST_USERS_FAILED
witherror
property is dispatched - run without error then action
REQUEST_USERS_SUCCEEDED
with propertyresult
is dispatched, where result is anything thatgetData
saga returns (nothing in our example😀 )
- throw an error during its run, action
- Action
REQUEST_USERS_COMPLETED
is dispatched at the end every time, no matter if runninggetData
failed or succeded
Little magic explanation:
Actions dispatched during processing of a saga are automatically created inside therunSagas
helper as a composition of the initial action (REQUEST_USERS
in our example) and one of the prefixes -_FAILED
,_SUCCEEDED
or_COMPLETED
.
Modules
Routing history
There is a routing history module for handling history in redux & react-router apps called routingHistory
import routingHistory from '@ackee/chris/routing-history';
Utilities
combineDependenciesHandlers(...routeHandlers) => combinedRouteHandlers
Helper to combine dependencies handlers for runRouteDependecies
. Accepts infinite number of handlers objects ({'template': sagaHandler})
and returns exactly one for usage in runRouteDependecies
. Supports same keys in the handlers objects
Usage
import { routeHandlers as usersHandlers } from 'Modules/users';
import { routeHandlers as reviewsHandlers } from 'Modules/reviews';
export const appHandlers = {
'/': homeSaga,
'/logout': function* () {
// ...
},
};
const combinedRouteHandlers = combineDependenciesHandlers(
appHandlers,
usersHandlers,
reviewsHandlers
);
runRouteDependencies(combinedRouteHandlers);
Each module (e.g. Modules/users
) may exports its own routeHandlers
object and the combineDependenciesHandlers
utility handles their merging.
HOC
fetchDependencies(config?: Config) => (Component) => ComponentThatFetchDependencies
High order component used to request data for wrapped component. If you wrap your page components with the HOC it will ensure that data it needs will be requested right after component render.
In default HOC calls fetch
function passed through props when component mounts and clear
before it unmounts. If it doesn't suit your needs, supply a config that define your own way of handling. All methods in config are optional and if they aren't supplied, default ones are used.
interface Config {
onLoad?: (props) => void;
onUnload?: (props) => void;
shouldReFetch?: (prevProps, nextProps) => boolean;
}
Example - Use with default config
const UsersListPageContainer = compose(
connect(
state => ({ users: state.users }),
dispatch => bindActionCreators({
fetch: requestUsers,
clear: deleteUsers
}, dispatch),
),
fetchDependencies(),
)(UsersListPage);
const App = () => (
<div>
<UserListPageContainer />
</div>
);
Example - Use with custom config
const UserDetailPageContainer = compose(
connect(
(state: State) => ({
user: state.user,
userId: state.selectedUserId
}),
dispatch => bindActionCreators({
requestUserDetail: requestUser,
clearUserDetail: clearUser,
}, dispatch),
),
fetchDependencies({
onLoad: ({ userId, requestUserDetail }) => {
requestUserDetail(userId);
},
onUnload: ({ clearUserDetail }) => {
clearUserDetail();
},
shouldReFetch: (prevProps, props) => {
return prevProps.userId !=== props.userId;
},
}),
)(UserDetailPage);
const App = () => (
<div>
<UserListPageContainer />
</div>
);
routeDependencies(config?: Config) => (Component) => ComponentWithRouteDependencies
Important prerequistence for using the HOC is that you must have
react-router
in your app.
HOC has same purpose and works almost same as fetchDependencies
with few exceptions.
- It wraps component also with
withRouter
HOC so all props receive also router'smatch
,location
andhistory
objects. - Its config is slightly different especially in method names. Look at the definition:
interface Config { onRouteEnter?: (props) => void; onRouteLeave?: (props) => void; shouldReRoute?: (prevProps, nextProps) => boolean; }
- Default implementation of
shouldReRoute
invoke refetch of data every time any part of url change. It's used until you provide your own.
Example - Use with default config
const UsersListPageContainer = compose(
connect(
state => ({ users: state.users }),
dispatch => bindActionCreators({
fetch: requestUsers,
clear: deleteUsers
}, dispatch),
),
routeDependencies(),
)(UsersListPage);
const App = () => (
<Router>
<div>
<Route path="/users" component={UserListPageContainer}/>
</div>
</Router>
);
Example - Use with custom config
const UserDetailPageContainer = compose(
connect(
(state: State) => ({ user: state.user }),
dispatch => bindActionCreators({
requestUserDetail: requestUser,
clearUserDetail: clearUser,
}, dispatch),
),
routeDependencies({
onRouteEnter: ({ match, requestUserDetail }) => {
requestUserDetail(match.params.id);
},
onRouteLeave: ({ match, clearUserDetail }) => {
clearUserDetail();
},
shouldReRoute: (prevProps, props) => {
return prevProps.match.params.id !=== props.match.params.id;
},
}),
)(UserDetailPage);
const App = () => (
<Router>
<div>
<Route path="/users/:id" component={UserDetailPageContainer}/>
</div>
</Router>
);