Simple library for helping created redux-normalized state.
ActionCreator + ActionSelector, reducers are created automatically
- Features
- Overview
- Examples
- Installation
- Usage
- Action
- Reducer
- Middleware
- Selector
- Contributions
- Changelog
- License
You don't need to create reducers for rest-api data
You should create reducers only for business front-end logic
- Simple actionCreator for any of your async library
- Normalization state
- Auto create reducers
- Simple single selector for all your actions
Redux-Tide - Do not force you to use only it,
This is a small library helping you create normalization, actions, reducers and selector
You can use it with any of other libraries
You can add redux tide in any even old/new project
Redux tide - it's concept for save your backend data, normalize, and selector for it
┌───────────┐
│ Some │
│Reducers...│ ┌──────────┐
└───────────┘ │ Some │ ┌───────────┐
╔═══════════╗ │ Selector │ │ Some │
╔════════╗ ┌────────────┐ ┌────────┐┌▶║ Actions ║ └──────────┘ ┌─▶│ Component │
║ Action ╠──▶ Async Rest ├──▶Response├┤ ║ Meta ╠─┐ │ └───────────┘
╚════════╝ └────────────┘ └────────┘│ ╚═══════════╝ │ ╔══════════╗ │
│ ╔═══════════╗ ├─▶║ Selector ╠─┘
│ ║Normalized ║ │ ╚══════════╝
└▶║ Data ║─┘
╚═══════════╝
blog - using with axios and REST api
blog-source - blog demo source code
different-entity-id-example
different-entity-id-source
merged-actions-data-example
merged-actions-data-source
To install the stable version:
npm install redux-tide --save
Your project should have normalizr, redux, react-redux, redux-thunk
npm install normalizr --save
npm install redux --save
npm install react-redux --save
npm install redux-thunk --save
You can connect to Gitter chat room
- You might install library
npm install redux-tide --save
-
Install normalizr, redux, react-redux, redux-thunk, you can use help Complementary Packages
-
Define entity-schema
// entity-schema.js import {schema} from 'normalizr' const postsSchema = new schema.Entity('posts') const commentsSchema = new schema.Entity('comments') postsSchema.define({ comments: [commentsSchema] }) commentsSchema.define({ post: postsSchema }) const appSchema = { commentsSchema, postsSchema } export { postsSchema, commentsSchema, appSchema }
-
Modify your store.js file
// store.js import {denormalize} from 'normalizr'; import {createReducers, setDefaultResponseMapper, setDenormalize} from 'redux-tide'; import {appSchema} from './entity-schema' // required setDenormalize(denormalize) // not required setDefaultResponseMapper((resp) => { // your response return resp.data }) // your store export default createStore( combineReducers({ // your reducers ...createReducers(...appSchema) }), initialState, composedEnhancers )
-
Ready! Now you can use it. Create action
You can connect to Gitter chat room
Example of RestApi class source
You can look entity-schema.js example from Usage section
import {createAction} from 'redux-tide';
import {del, get, post, put} from '../RESTApi'
import {postsSchema} from 'entity-schema';
import {OPEN_EDIT} from './action-types'
/**
* createAction argumnents
*
* @param {String|Object} actionSchema - normalizr actionSchema item
* @param {Function|Promise} actionMethod
* @param {String|Function} [queryBuilder=undefined]
* @param {Function} [responseMapper=_defaultResponseMapper||callback from setDefaultResponseMapper]
**/
// simple action
export const getAllPosts = createAction(
postsSchema,
get,
`posts?_embed=comments&_order=desc` // simple static url
)
// warning, please read "Create one action for different entity id" section
export const getPostById = createAction(
postsSchema,
get,
postId => `posts/${postId}?_embed=comments` // url with property postId
)
// warning, please read "Create one action for different entity id" section
export const delPostById = createAction(
postsSchema,
del,
postId => `posts/${postId}` // url with property postId
)
// warning, please read "Create one action for different entity id" section
export const updatePostById = createAction(
postsSchema,
put,
(postId, data) => [
`posts/${postId}`, // url with property
undefined, // query params
data // put body
]
)
export const createNewPost = createAction(
postsSchema,
post,
data => [
`posts`, // static url
undefined, // query params
data // post body
]
)
// basic redux action can be use
export const openEditPost = (postId) => {
return {
type:OPEN_EDIT,
postId
}
}When you need to force a clear state of action, please use
dispatch(createNewPost.empty())OR
dispatch(getPostById.withPrefix(props.postId).empty())Please read Create one action for different entity id section and Other options to create an action
And look examples:
different-entity-id-example
different-entity-id-source
If you want to create 1 action get || post || put || delete
for work with single entity but multiple entity ids, for example: GET post/:postId
You should be use action.withPrefix method - it's generate new uniq action id and new uniq action reducer state
For details you can look example:
different-entity-id-example
different-entity-id-source
if you dont't make it - your next call dispatch(getPostById(nextPostId))
overwrite your preview call data dispatch(getPostById(prevPostId))
// actions.js
import {createAction} from 'redux-tide';
import {del, get, post, put} from '../RESTApi'
import {postsSchema} from 'entity-schema';
// write main action
export const getPostById = createAction(
postsSchema,
get,
postId => `posts/${postId}?_embed=comments` // url with property postId
)
// component.js
import {getActionData} from 'redux-tide';
import {getPostById} from './actions';
// WRONG connect!
export default connect(
// your selector does not have uniq post Id, so data is rewrited
(state, props) => getActionData(getPostById),
dispatch => {
// your selector does not have uniq post Id, so data is rewrited
fetch: (postId) => dispatch(getPostById(postId))
}
)(SomeComponent)
// CORRECT connect
// you can use this connect with different postId
export default connect(
// selector get state of getPostById but reducer key named with postId
(state, props) => getActionData(getPostById.withPrefix(props.postId)),
dispatch => {
// action call getPostById but dispatch TYPE make with prefix postId
fetch: (postId) => dispatch(getPostById.withPrefix(postId)(postId))
}
)(SomeComponent)
// common-component.js
import React, {Component} from 'react'
import {PostFormComponent} from './component'
export default class ComponentWrapper extends Component {
render(){
return(<PostFormComponent postId={this.props.postId}/>)
}
}import {getActionData} from 'redux-tide';{String} actionId - your action id
{*} sourceResult - your source response from server (not mapped response)
{String} status - pending|success|error
{Number} time - timestamp of action
{Boolean} hasError - has error or not
{String} errorText - text of error
{Boolean} isFetching - status === 'pending'
{Object|Array} payload - denormalized response for current action
{Object|Array} prevPayload - denormalized previous response
import {getActionData} from 'redux-tide';
import {
createNewPost,
delPostById,
getAllPosts,
openEditPost
} from './actions';
export default connect(
// single selector function for all your actions
(state, props) => getActionData(getAllPosts)
)(SomeComponent)
export default connect(
(state, props) => ({
table: getActionData(getAllPosts)(state, props)
})
)(SomeComponent)
// create custom selectors
const makeGetMergedActionData = (postId) => {
return createSelector(
[
getActionData(updatePostById.withPrefix(postId)),
getActionData(getPostById.withPrefix(postId)),
someSelector // your selector
],
(updateData, fetchData, someData) => {
updateData = updateData || {}
fetchData = fetchData || {}
const sortedDataByUpdateTime = [updateData, fetchData].sort(item => item.time)
return sortedDataByUpdateTime.reduce((memo, item) => {
return Object.assign(memo, item)
}, {someData})
}
)
import {
createNewPost,
delPostById,
getAllPosts,
openEditPost
} from './actions';
const createNewPostActionId = createNewPost.actionId()
// delete post using with prefix (post id query parameter), so need check parentActionId
const delPostByIdParentActionId = delPostById.actionId()
// action id if you called delPostById.withPrefix(postId), where postId === 5
const delPostId5ActionId = delPostById.withPrefix(5).actionId()
export const middleware = store => next => action => {
const result = next(action)
switch (action.actionId) {
// all actions createNew post
case createNewPostActionId:
if (action.status === 'success') {
store.dispatch(openEditPost(action.payload))
store.dispatch(getAllPosts())
}
break
case delPostId5ActionId:
if (action.status === 'success') {
// post with id 5 is was deleted success
}
}
// we used action delPostById with prefix postId
// for example dispatch(delPostById.withPrefix(postId)(postId)
// so, actionId has postId in actionId
// and if you want track all calls with any postId you hould used parentActionId property
// parentActionId - it's actionId from action which was called .withPrefix(*)
// so if you called delPostById.withPrefix()..., parrent action id - it's action id delPostById
switch (action.parentActionId) {
case delPostByIdParentActionId:
if (action.status === 'success') {
store.dispatch(getAllPosts())
let delPostCurrentActionId = delPostById.withPrefix(action.payload).actionId()
// it's actionid current deleted post === action.actionId
}
break
}
}For details you can look example:
different-entity-id-example
different-entity-id-source
This methods returns same action
But generate new uniq dispatch type and new uniq action state
You should be call .withPrefix, .withName, .clone when you are dispatch event and use getActionData
dispatch(getUserAction.withPrefix(userId)(userId))
connect(
(state)=> getActionData(getUserAction.withPrefix(userId))
)
// And this methods can chain calls
export const getUserAction = createAction(
user,
get,
'user',
(resp) => {resp.data}
).withName('user')
dispatch(getUserAction.withPrefix(userId)) // action type id and state key name includes user + userId
// OR
dispatch(getUser.withName('user').withPrefix(userId))
// AND selector
getActionData(getUser.withName('user').withPrefix(userId))// calling url 'user' but replace backend success response to resp.data
// You always can be get source response data
// from selector getActionData property sourceResult
export const getUserAction = createAction(
user,
get,
'user',
(resp) => {resp.data}
)// you can pass multi level functions or promises
// (args) => (dispatch, getState) => (dispatch, getState) => (dispatch, getState) => ...
// calling url 'user/${userId}'
export const getUserAction = createAction(
user,
get,
(userId) => {
return (dispatch, getState)=>{
// return Promise (axios call or other)
}
}
)// actions.js
export const get = (url) => {// returns Promise ajax call}
// simple action used custom method for getting data
export const getUserAction = createAction(user, () => {
return new Promise((resolve, reject) => {
// cookie|local storage|other get data
resolve({
//data
})
})
})
// if you want to best action store name in redux,
// you should used this pattern
// calling url 'user/${userId}'
export const getUserAction = createAction(
user,
get,
(userId) => `user/${userId}`
).withName('UsersAction')
// calling url 'user/${userId}'
// and post data (if you are using axios) {name, phone, email}
export const getUserAction = createAction(
user,
post,
(userId) => [
`user/${userId}`,
undefined,
{name, phone, email}
]
)
// you can pass multi level functions or promises
// (args) => (dispatch, getState) => (dispatch, getState) => (dispatch, getState) => ...
// calling url 'user/${userId}'
export const getUserAction = createAction(
user,
get,
(userId) => {
return (dispatch, getState)=>{
return new Promise((resolve) => {
resolve(`user/${userId}`)
})
}
}
)
// calling url 'user' but replace backend success response to resp.data
export const getUserAction = createAction(
user,
get,
'user',
(resp) => {resp.data}
)
// using with multiple entity ids (make two action ids and stores from simgle action)
export const getUserByIdAction = createAction(
user,
get,
userId => `users/${userId}`
)
class UserComponent extends Component {
// ...
}
const UserContainer = connect(
(state)=>({
userData: getActionData(getUserByIdAction.withPrefix(userId))(state, props)
}),
(dispatch)=>({
getUser:(userId) => (dispatch(getUserByIdAction.withPrefix(userId)(userId))
})
)(UserComponent)// when you did action, you can use action public methods
export const getAllPosts = createAction(
postsSchema,
get,
`posts?_embed=comments&_order=desc` // simple static url
)
// you can call:
// getAllPosts.type()
// getAllPosts.getEntityId(postObj)
// getAllPosts.getSchema()
// getAllPosts.clone()
// getAllPosts.withPrefix(customPrefixId) // customPrefixId might be postId
// getAllPosts.withName(yourCustomName)
// getAllPosts.empty()For details, please look source
Use GitHub issues for requests.
I actively welcome pull requests; learn how to contribute.
MIT