![require(esm) Backported to Node.js 20, Paving the Way for ESM-Only Packages](https://cdn.sanity.io/images/cgdhsj6q/production/be8ab80c8efa5907bc341c6fefe9aa20d239d890-1600x1097.png?w=400&fit=max&auto=format)
Security News
require(esm) Backported to Node.js 20, Paving the Way for ESM-Only Packages
require(esm) backported to Node.js 20, easing the transition to ESM-only packages and reducing complexity for developers as Node 18 nears end-of-life.
redux-saga-mate
Advanced tools
Allow you building react and redux based web apps with less pain.
Allow you building react and redux based web apps with less pain, by removing the needs for writing lots of action types, reducers.
------------------------------
| presentational components | ^
------------------------------- |
| container components (auto) | |
------------------------------- |
| react-redux connect() | |
------------------------------- |
| selectors (js) | |
------------------------------- |
| redux store (state) (js) | |
------------------------------- |
| redux reducers (js) | |
------------------------------- data flow
| redux action payloads (js) | ^
------------------------------- |
| normalization (js) | |
------------------------------- |
| remote API calls (js) | |
------------------------------- |
| Web API, WebSocket Endpoints | |
------------------------------- |
| Server State | |
------------------------------
action
s are about what happend, it's not about "what should be done", even if they were named in verbs.reducer
's job, that about "what should be done" and "how it should be done".container
files you put in the "containers" directory are not actual container
s, they are just connecting logics, actual container
s are created automatically by connect(YourComponent)
, you can only see them in the browser's Developer Tools
.container
files. Because they are about the UI.redux-thunk
changes the origin conceptual model of the action
, by functions, and functions always about "what should be done", or "how it should be done".action
is not equal to action types. Action Type
+ Action Payload
= Action Instance
.redux
, redux-thunk
, redux-saga
, tell you track the async action state by action type
, this is not what you want, in most of the time.store
, it also means your components are fully controlled components
, the states and callbacks(handlers) are all passed as props.normalization
strategy is mean to solve this problem, even if that may not work perfectly. I hope you know how to use the normalizr
library.https://hanzhixing.github.io/redux-saga-mate/
Install the package.To use with node:
$ npm install redux-saga-mate --save
Install peer dependencies, you may already have these be installed.
npm install react redux redux-saga recompose reselect redux-actions
src/
├── actions
│ └── types.js
├── api
│ └── index.js
├── components
│ ├── App
│ │ └── index.jsx
│ └── PostList
│ ├── index.jsx
│ └── index.module.scss
├── config.js
├── connects
│ └── PostList
│ ├── index.js
│ └── selectors.js
├── index.css
├── index.js
├── reducers
│ ├── index.js
│ └── ui
│ ├── index.js
│ └── posts.js
├── sagas
│ └── index.js
├── store
│ ├── configureStore.js
│ └── index.js
└── utils
└── index.js
{
session: { <--- current session based infomations
username: ...,
},
entities: { <--- normalized entities, again, learn to use the normalizr library
posts: {
1: {
...
}
2: {
...
}
}
},
ui: { <--- relation infomations between the entities and the UI.
home: {
latestPosts: {
...
}
}
}
actions: { <--- all action infomations
}
}
{
type: 'YOUR_ACTION_TYPE',
payload: {...any infomation as object...},
error: true or false,
meta: { // this infomation will be managed automatically
id: uniq_hash(type + payload),
pid: parentOf(id), // not used yet
ctime: ISO8601,
utime: ISO8601,
phase: 'started'|'running'|'finished',
progress: integer between 1~100
uniq: true or false,
}
}
Recommend normalized your api data in the API layer.
{
request: {
data: {...}, // for POST, PUT, PATCH body (should be plain object)
params: {...}, // hint: react-router params
query: {...}, // hint: querystring.parse(location.search)
},
response: {
...normalize(data, schema), // see normalizr
}
}
export const CLEANUP = 'CLEANUP';
// You need not split this to ASYNC_GET_MANY_POST_REQUEST, ASYNC_GET_MANY_POST_SUCCESS, ASYNC_GET_MANY_POST_FAILURE
export const ASYNC_GET_MANY_POST = 'ASYNC_GET_MANY_POST';
Normalize your data in the API layer. It's the only right place.
export const restfulGetManyPosts = args => fetch(...).then(data => normalize(data, YOUR_SCHEMA))
import {combineReducers} from 'redux';
import {concat, difference} from 'lodash/fp';
import {createActionsReducer, createEntitiesReducer, groupByComposeByEntityType} from 'redux-saga-mate/lib/reducer';
// there are only these two operations for state updating.
import {UPDATE, DELETE} from 'redux-saga-mate/lib/operation';
import * as ActionTypes from '../actions/types'; // It's ok, if you want to import action types explicitly.
// The keys is your entities keys in the store.
const EntityActionMap = {
posts: {
// the value part can be one single OPERATION(string), or tuple [OPERATION, yourMergeFunction]
[ActionTypes.ASYNC_GET_MANY_POST]: [
UPDATE,
// @see the 'mergeDeepWith' from 'ramda'
(k, l, r) => (k === 'commenters' ? concat(l, difference(r, l)) : r),
],
[ActionTypes.ASYNC_DELETE_ONE_POST]: DELETE,
[ActionTypes.ASYNC_PATCH_ONE_POST]: UPDATE,
...
},
users: {
...
},
...
// add your mapping rules instead of writing reducers
};
const locators = {
// define possible paths to entities in your action payload
UPDATE: [
['response', 'entities'],
['entities'],
['entities'],
],
// paths to primaryKey in your action payload, which will be used to delete the entity
DELETE: [
['request', 'params', 'id'],
],
};
export default combineReducers({
// tuple [ACTION_TYPE_FOR_CLEANUP, YOUR_ASYNC_ACTION_TYPE_REGEX]
actions: createActionsReducer([ActionTypes.CLEANUP, /^ASYNC_/]),
entities: combineReducers(
groupByComposeByEntityType(
createEntitiesReducer(locators, EntityActionMap),
{
...
/// put your own legacy reducers here, they will executed at the end of reducing
...
},
),
),
...
// If you are creating new app, codes above can be written like bellow
entities: combineReducers(createEntitiesReducer(locators, EntityActionMap)),
...
});
import {all, takeEvery} from 'redux-saga/effects';
import {makeCreateDefaultWorker} from 'redux-saga-mate/lib/saga';
import * as ActionTypes from '../actions/types';
import * as Api from '../api';
// you need to tell the Error Type for failure situation of the async action.
const createDefaultWorker = makeCreateDefaultWorker([MyError, ActionTypes.CLEANUP]);
// If you want to clear action state when success, you pass option object as the second argument.
// const createDefaultWorker = makeCreateDefaultWorker([MyError, ActionTypes.CLEANUP], {autoclear: true});
// Notice!
// If you need more complicated logic controls then the default worker saga,
// you need to implement your own worker sagas.
export default function* () {
yield all([
// create a worker saga with your remote call promise, you need only one line code.
takeEvery(ActionTypes.ASYNC_GET_MANY_POST, createDefaultWorker(Api.restfulGetManyPosts)),
// If you need infomations from state, before run the promise, you can prepare the payload.
// What you return will pass in to the remote call.
takeEvery(ActionTypes.ASYNC_GET_ONE_USER_BY_POST_ID, createDefaultWorker(
Api.getOneUser,
(state, action) => {
const {postId} = action.payload;
const {author} = state.entities.posts[postId];
return {id: author};
},
// If you want to disable action state autoclearing just for this worker
// {autoclear: false}
)),
]);
}
import {connect} from 'react-redux';
import {compose, lifecycle, withState, mapProps} from 'recompose';
import {createSelector} from 'reselect';
import {createAction} from 'redux-actions';
import {createAsyncAction, idOfAction} from 'redux-saga-mate/lib/action';
import {
// You can use this,
withAsyncActionStateHandler,
// or this.
createAsyncActionContext,
// How they are different from each other, go on reading to the end.
} from 'redux-saga-mate/lib/hoc';
import {createSelectActions} from 'redux-saga-mate/lib/selector';
import PostList from '../../components/PostList';
import {selectPosts, selectPostsBuffer, selectModalAuthor} from './selectors';
import * as ActionTypes from '../../actions/types';
// The selector below is the same as the selector you got from reselect's createSelector.
const selectActions = createSelectActions(
(state, props) => state.actions, // provide actions selector from store
(state, props) => props.actionIds, // provide actionIds selector maybe from props
);
const makeSelectProps = () => createSelector(
selectPosts,
// Once your component is wrapped with 'withAsyncActionStateHandler', you can select out the actions.
// So as when you wrapped with 'withAsyncActionContextConsumer' created by 'createAsyncActionContext'.
selectActions,
(items, transients) => ({
items: posts,
transients, // in the ui component, you can examine the action by 'transients.onPage[page]'
...
}),
);
const makeMapStateToProps = () => {
const selectProps = makeSelectProps();
return (state, props) => selectProps(state, props);
};
const mapDispatchToProps = (dispatch, {onTrackAsyncAction}) => ({
onPage: page => {
// 1. Make your action Async with 'createAsyncAction'.
// 2. dispatch it.
// 3. take the action id with 'idOfAction'
const action = dispatch(createAsyncAction(ActionTypes.ASYNC_GET_MANY_POST)({
page,
}));
// you can pass single string, or path in array form for the first argument
// Seconds is the Action Id.
onTrackAsyncAction(['onPage', page], idOfAction(action));
},
});
const withRedux = connect(makeMapStateToProps, mapDispatchToProps);
export default compose(
...
withRedux,
...
)(PostList);
You have two options.
Use withAsyncActionStateHandler
const withAsyncAction = withAsyncActionStateHandler(({actionIds, setActionId, unsetActionId}) => ({
actionIds,
onTrackAsyncAction: setActionId,
onUntrackAsyncAction: unsetActionId,
}));
export default compose(
...
withAsyncAction,
...
withRedux,
...
)(PostList);
createAsyncActionContext
// You may want to create these two hoc from a seperated file and import the provider or consumer.
// The benefit use context is you need not pass the props along the tree.
const {withAsyncActionContextProvider, withAsyncActionContextConsumer} = createAsyncActionContext();
export default compose(
...
withAsyncActionContextProvider,
...
withAsyncActionContextConsumer,
mapProps(({actionIds, setActionId, unsetActionId}) => ({ // It is just recompose's mapProps
actionIds, // off course the 'actionIds' must be matched with the key in the action selector: selectActions
onTrackAsyncAction: setActionId, // You can map the props like this.
onUntrackAsyncAction: unsetActionId,
}))
withRedux,
...
)(PostList);
const mapActionProps = ({actionIds, setActionId, unsetActionId}) => ({
actionIds, // off course the 'actionIds' must be matched with the key in the action selector: selectActions
onTrackAsyncAction: setActionId, // You can map the props like this.
onUntrackAsyncAction: unsetActionId,
})
export default compose(
...
withAsyncActionContextConsumer,
mapProps(mapActionProps), // It is just recompose's mapProps, you can use withProps or mapProps.
withRedux,
...
)(PostList);
FAQs
Allow you building react and redux based web apps with less pain.
The npm package redux-saga-mate receives a total of 6 weekly downloads. As such, redux-saga-mate popularity was classified as not popular.
We found that redux-saga-mate demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
require(esm) backported to Node.js 20, easing the transition to ESM-only packages and reducing complexity for developers as Node 18 nears end-of-life.
Security News
PyPI now supports iOS and Android wheels, making it easier for Python developers to distribute mobile packages.
Security News
Create React App is officially deprecated due to React 19 issues and lack of maintenance—developers should switch to Vite or other modern alternatives.