In "Basic Reducer Structure" in Redux docs, It is stated that:
A typical app's state shape might look roughly like:
{
domainData1 : {},
domainData2 : {},
appState1 : {},
appState2 : {},
ui : {
uiState1 : {},
uiState2 : {},
}
}
Or
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....},
}
}
Now I use combineReducers and I need to know how can I achieve this structure
considering combineReducers behavior which is that a corresponding key is set in state object for each reducer that is passed to combineReducers.
Do I have to have a reducer created for domainData1, domainData2, appState1, appState2 and ui?
Or there is another way for defining the shape of state like this?
Is it possible to disable combineReducers's behavior of adding key for each reducer to state?
combineReducers takes an object full of slice reducer functions, and
creates a function that outputs a corresponding state object with the
same keys.
For redux documentation this is definition of combine reducer.
Do I have to have a reducer created for domainData1, domainData2, appState1, appState2 and ui?
Yes, define reducer for each one and combine the reducer for each of them.
Like :
{
domainData1: domainData1Reducer,
domainData2: domainData2Reducer,
appState1 : appState1Reducer,
appState2 : appState1Reducer,,
ui: uiReducer,
}
Is it possible to disable combineReducers's behavior of adding key for each reducer to state
No, with combine Reducer you can't. But you can write your implementation for this.
Or there is another way for defining the shape of state like this?
Yes, we can do this too in single Reducer also.If you want.
// don't provide keys to reducers that don't supply them
const filterReducer = (reducer) => {
let lastState = undefined;
return (state, action) => {
if (lastState === undefined || state == undefined) {
lastState = reducer(state, action);
return lastState;
}
var filteredState = {};
Object.keys(lastState).forEach( (key) => {
filteredState[key] = state[key];
});
var newState = reducer(filteredState, action);
lastState = newState;
return newState;
};
}
This is what I did:
First I created simpleDomainData1, simpleDomainData2, entityType1 , entityType2 , and uiSection1 and uiSection2 .
And then I Combined entityType1 and entityType2 like this:
const entities = combineReducers({
entityType1,
entityType2,
});
const ui = combineReducers({
uiSection1,
uiSection2,
})
or instead you can create your own combineReducers function and use that:
const entities = (state, action) => {
entityType1: entityType1(state.entityType1),
entityType2: entityType2(state.entityType2)
}
const ui = (state, action) => {
uiSection1: uiSection1(state.uiSection1),
uiSection2: uiSection2(state.uiSection2)
}
And after that:
const allReducers = combineReducers({
simpleDomainData1,
simpleDomainData2,
entities,
ui
});
Now the state shape will look like this:
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....},
}
}
Related
We have a really large codebase with 271 class components and the old redux (not redux toolkit).
We are in the process of migrating to redux toolkit - and adoption RTK-Query as our async state manager. We will migrate redux-saga based functionality to rtk-query and trim our reducers.
For functional components it's very easy to do both things
get data, loading state
dispatch the action to ask for this data.
const {data: posts, isLoading, isError, isSuccess } = usePosts();
But how do I do this in a class based component...
componentDidMount(){
//what to dispatch here ?
}
render(){
const {data: posts, isLoading, isError, isSuccess } = fromWhichPlace;
}
We specifically cover this topic in our docs:
https://redux-toolkit.js.org/rtk-query/usage/usage-without-react-hooks
https://redux-toolkit.js.org/rtk-query/usage/examples#react-class-components
Per those pages, you'd need to do something along the lines of:
const mapState = (
state: RootState,
ownProps: RouteComponentProps<{ id: string }>
) => ({
id: Number(ownProps.match.params.id),
post: endpoints.getPost.select(Number(ownProps.match.params.id))(state),
updatePostState: endpoints.updatePost.select(ownProps.match.params.id)(state), // TODO: make selectors work with the requestId of the mutation?
deletePostState: endpoints.updatePost.select(ownProps.match.params.id)(state)
});
const mapDispatch = {
getPost: endpoints.getPost.initiate,
updatePost: endpoints.updatePost.initiate,
deletePost: endpoints.deletePost.initiate
};
const connector = connect(mapState, mapDispatch);
type PostDetailProps = ConnectedProps<typeof connector> & RouteComponentProps;
export class PostDetailComp extends React.Component<PostDetailProps> {
state = {
isEditing: false
};
componentDidMount() {
const { id, getPost } = this.props;
getPost(id);
}
componentDidUpdate(prevProps: PostDetailProps) {
const { id, getPost } = this.props;
if (id !== prevProps.id) {
getPost(id);
}
}
render() {
const { isEditing } = this.state;
const {
// state
id,
post: { data: post, isLoading: isPostLoading },
updatePostState: { isLoading: isUpdateLoading },
deletePostState: { isLoading: isDeleteLoading },
// actions
updatePost,
deletePost
} = this.props;
// snip rendering logic
}
export const PostDetail = connector(PostDetailComp);
However, we would strongly recommend that you convert these components to function components instead, and use the auto-generated query hooks! It will be much simpler and easier to use, and the hooks have a lot of built-in functionality that will be hard to replicate by hand in class components.
I am trying to append action.payload to my state. However, push methods adds action.payload.length to my state instead of appending the entire array! What am I doing wrong?
const initialState = { users: [] };
export const usersSlice = createSlice({
//other code.
,
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
console.log(current(state));
state.users = state.users.push(...action.payload);
console.log(action.payload);
console.log(current(state));
// this one works.
// state.users = state.users.concat(action.payload);
});
},
});
// selector
export const selectUserById = (state, userId) =>
state.users.users.find((user) => user.id === userId);
This is the error I get (referring to selector):
TypeError: state.users.users.find is not a function
And this is my console:
// This is my state.
{
"users": []
}
// this is action.payload. Which is an array of 3 objects.
[
{
obj1
},
{
obj2
},
{
obj3
}
]
// This is the state after using push:
{
"users": 3
}
Well, such a silly mistake.
As y'all know, push method does not return anything. Thus, it made no sense for me to try to assign it to my state. concat method on the other hand, returns a new array. That's why it worked.
Here is what I changed:
state.users.push(...action.payload);
There is not state.users = anymore.
I have a redux store with multiple teams.
const store = {
selectedTeamId: 'team1';
teams: {
team1: { ... },
team2: { ... },
team3: { ... },
},
};
At any given time a teamId is set.
Now given that I must select the team using the ID each time I call mapStateToProps(), I feel this is cumbersome.
Instead of doing this all the time:
mapStateToProps({ selectedTeamId, teams }) {
return {
team: teams[selectedTeamId],
}
}
Can I pre-process the store using some middleware instead of repeating this pattern in map state to props?
Approach suggested by Redux docs is to create a selector for currently active team and reuse it across all components
// selector itself is a pure function of state
// usually put in separate file, or in file with reducer
const activeTeamSelector = state => state.teams.teams[state.teams.selectedTeamId]
// in connect
const mapStateToProps = (state) => ({
activeTeam: activeTeamSelector(state),
})
That, of course, if you are using combineReducers and teams reducer is called teams in state. If you aren't, and selectedTeamId and teams are contained right in your store, following will work
const activeTeamSelector = state => state.teams[state.selectedTeamId]
Notice how I only had to change selector for this, and not every mapStateToProps in all the components
read more about Normalizing Store State and Computing Derived Data in Redux docs
Using a middleware for this scenario isn't performant (if I understood your question correctly :) ). I will outline 3 options you can use to achieve this:
Option 1
return both selectedTeamId and teams in mapStateToProps, this will allow you to find the team you need for each selected id:
mapStateToProps({ selectedTeamId, teams }) {
return {
selectedTeamId,
teams
}
}
That way you can access these props in render:
render() {
const { teams, selectedTeamId } = this.props;
return <Team team={teams.find(team => team.id === selectedTeamId)} />
}
Note: <Team /> is just a component I made for demonstration
Option 2
you can use reselect library to avoid recomputing this prop:
import { createSelector } from 'reselect'
const teams = state => state.teams;
const selectedTeamId = state => state.selectedTeamId;
const subtotalSelector = createSelector(
teams,
selectedTeamId,
(teams, selectedTeamId) => items.find(team => team.id === selectedTeamId)
)
Option 3
Create an action that will dispatch 'SELECT_TEAM' with the teamId
export function setSelectedTeam(id) {
return { type: types.SELECT_TEAM, payload: id };
}
Create a reducer for that type and return selectedTeam:
[types.SELECT_TEAM]: (state, payload)=> {
return {
...state,
selectedTeam: state.teams.find(team => team.id === payload.id)
};
},
That way you can have a selector for selectedTeam
export const getSelectedTeam = state => state.selectedTeam;
Hope it helps
I eventually used reselect, with thanks to the recommendation of #jank.
One of things I wanted to do was abstract away the need for selectors to appear in mapStateToProps. In order to do that, I wrapped redux connect. This allows insertion of a denormalizer function before mapStateToProps.
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
const getActiveTeamId = state => state.activeTeamId;
const getAllTeams = state => state.teams;
const teamSelector = createSelector(
getActiveTeamId,
getAllTeams,
(activeTeamId, teams) => teams[activeTeamId],
);
function denormalizer(mapStateToProps) {
return state => {
return mapStateToProps({ team: teamSelector(state) });
};
}
export default function reConnect(mapStateToProps = null, actions = null) {
const denormalizedMapStateToProps = denormalizer(mapStateToProps);
return function callConnect(Component) {
return connect(denormalizedMapStateToProps, actions)(Component);
};
}
My component get some properties via props with the function:
const mapStateToProps = state => {
const { entities: { keywords } } = state
const {locale} = state
return {
keywords: keywords[locale]
}
}
I got state keywords using ajax, in the same component:
componentDidMount() {
this.props.loadKeywords()
}
My component gets rendered twice. First, before the ajax resolves, so in my render method I got undefined:
render() {
const { keywords } = this.props.keywords
...
Which is the proper way to solve it? I changed componentDidMount to componentWillMount without success.
Right now, based on the real-world example, I have initialized keywords state with an empty object:
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
My reducer:
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import merge from 'lodash/merge'
import locale from './modules/locale'
import errorMessage from './modules/error'
import searchText from './modules/searchText'
// Updates an entity cache in response to any action with response.entities.
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
export default combineReducers({
locale,
router,
searchText,
errorMessage,
entities
})
My action:
import { CALL_API, Schemas } from '../middleware/api'
import isEmpty from 'lodash/isEmpty'
export const KEYWORDS_REQUEST = 'KEYWORDS_REQUEST'
export const KEYWORDS_SUCCESS = 'KEYWORDS_SUCCESS'
export const KEYWORDS_FAILURE = 'KEYWORDS_FAILURE'
// Fetches all keywords for pictos
// Relies on the custom API middleware defined in ../middleware/api.js.
function fetchKeywords() {
return {
[CALL_API]: {
types: [ KEYWORDS_REQUEST, KEYWORDS_SUCCESS, KEYWORDS_FAILURE ],
endpoint: 'users/56deee9a85cd6a05c58af61a',
schema: Schemas.KEYWORDS
}
}
}
// Fetches all keywords for pictograms from our API unless it is cached.
// Relies on Redux Thunk middleware.
export function loadKeywords() {
return (dispatch, getState) => {
const keywords = getState().entities.keywords
if (!isEmpty(keywords)) {
return null
}
return dispatch(fetchKeywords())
}
}
All based on the Real world redux example
My Solution
Given initial state to keywords entity. I'm getting json like this through ajax:
{'locale': 'en', 'keywords': ['keyword1', 'keyword2']}
However as I use normalizr with locale as id, for caching results, my initial state is as I describe in the reducer:
function entities(state = { users: {}, repos: {}, keywords: { 'en': { 'keywords': [] } } }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
What I don't like is the initial if we have several languages, also remembering to modify it if we add another language, for example fr. In this
keywords: { 'en': { 'keywords': [] } }
should be:
keywords: { 'en': { 'keywords': [] }, 'fr': { 'keywords': [] } }
This line looks problematic:
const { keywords } = this.props.keywords
It's the equivalent of:
var keywords = this.props.keywords.keywords;
I doubt that's what you intended.
Another thing worth checking is keywords[locale] in your mapStateToProps() which will probably initially resolve to undefined. Make sure your component can handle that, or give it a sensible default.
I'm a beginner in react / redux.
I've finished a basic component <HeatMap /> in my app, with its actions / reducer / store and it works well.
And I'll render another <HeatMap /> with different settings (props).
What I'm trying to do is to separate this 2 component, because when i dispatch an update action in one, the other one performed it simultaneously.
Question 1
I tried this to separate the states in store
import heatMap from './heat-map1'
import {combineReducers} from 'redux';
export let reducers = combineReducers({
heatMap1: heatMap,
heatMap2: heatMap
});
combineReducers and connectthe 2 heatmap in different object in store
export default connect((state)=> {
return {
onState: state.heatMap1.onState,
config: state.heatMap1.config
}
})(CHMSHeatMap1)
and
export default connect((state)=> {
return {
onState: state.heatMap2.onState,
config: state.heatMap2.config
}
})(CHMSHeatMap2)
is this correct?
Question 2
Because 2 component both react when action is dispatched
I'm thinking about separating the shared actions, but I don't think it's a good idea. Or maybe the issue is not here.
So can you tell me what cause this problem and how to solve it?
Here are my reducer
import * as actionTypes from '../actions/heat-map';
import Immutable from 'immutable';
const onState = {
fetching: 'FETCHING',
error: 'ERROR',
drawn: 'DRAWN'
};
const initialState = {
onState: onState.fetching,
config: {}
};
export default function heatMapReducer(state = initialState, action) {
let immutableState = Immutable.fromJS(state);
switch (action.type) {
case actionTypes.INITIALIZING:
return immutableState.set('onState', onState.drawn).set('config', action.payload.initConfig).toJS();
case actionTypes.FETCH_DATA_REQUEST:
return immutableState.set('onState', onState.fetching).toJS();
case actionTypes.FETCH_DATA_SUCCESS:
return immutableState.set('onState', onState.drawn).setIn(['config','series',0,'data'],Immutable.fromJS(action.payload.mapData.data)).toJS();
case actionTypes.FETCH_DATA_FAILURE:
return immutableState.set('onState', onState.error).set('config', action.payload.mapData).toJS();
default:
return state;
}
}
Action is simple
export function initializeConfig(initConfig) {
return {
type: INITIALIZING,
payload: {
text: 'Initializing',
initConfig
}
}
}
export function requireMapData() {
return {
type: FETCH_DATA_REQUEST,
payload: {
text: 'Loading'
}
};
}
..........
//Async Action for fetching map data and redraw the map
export function fetchMapData(address) {
return function (dispatch) {
//dispatch requireMapData action to set the map in loading state
dispatch(requireMapData());
return fetch(address)
.then(fetchUtil.checkHttpStatus) //check if 404
.then(fetchUtil.parseJSON)
.then(mapData => dispatch(fetchDataSucceed(mapData)))
.catch(error => dispatch(fetchDataFailed(error)));
}
}
Thank you my friend.
You cannot duplicate your reducers in the manner you've depicted. Both are going to respond in the exact same way to the exact same actions.
The solution is to have all of your heat map data in the same reducer state. e.g.
const initialState = {
heatMap1: {},
heatMap2: {}
};
export default heatmap(state = initialState, action) {
// etc
Now if you want to use the same actions for both heat maps, you'll need to have an action property specifying which heap map you're targeting. If you have several heat maps, I'd recommend an array of heat maps with each action containing an index or id to target a particular heat map. e.g.
function updateHeatMap(index, value) {
return {
type: UPDATE_HEATMAP,
index: index,
value: value
}
}
You can also take a look at the multireducer module (https://github.com/erikras/multireducer). It was designed to solve exactly the scenario you propose.
So you would be able to configure your store as such:
import multireducer from 'multireducer';
import heatMap from './heat-map1'
import {combineReducers} from 'redux';
export let reducers = combineReducers({
multireducer: multireducer({
heatMap1: heatMap,
heatMap2: heatMap
})
});
After that, you would then need to use connectMultireducer() instead of redux's standard connect() in order to connect the specific slice of the store to particular components like so:
export default connectMultireducer((state)=> {
return {
onState: state.heatMap.onState,
config: state.heatMap.config
}
})(CHMSHeatMap)
And finally in order to get the correct part of the state to each of those components you would pass in the key when rendering them as such:
<CHMSHeatMap multireducerKey="heatMap1"/>
<CHMSHeatMap multireducerKey="heatMap2"/>
Obviously it's better to read the actual docs at the multireducer repo, but that should give a brief overview. Basically the module is just abstracting the process of adding a key-based lookup to each reducer that is created through the multireducer function.
I suggest original concept of multireducer working without any libraries.
The base idea is unique Symbol action types and self-contained Redux-module like this:
import * as services from './../../../api/services';
const initialState = {
list: [],
};
function getListReducer(state, action) {
return {
...state,
list: action.payload.list,
};
}
function removeItemReducer(state, action) {
const { payload } = action;
const list = state.list.filter((item, i) => i !== payload.index);
return {
...state,
list,
};
}
export default class List {
constructor() {
// action types constants
this.GET_LIST = Symbol('GET_LIST');
this.REMOVE_ITEM = Symbol('REMOVE_ITEM');
}
getList = (serviceName) => {
return async (dispatch) => {
const list = await services[serviceName].get();
dispatch({
type: this.GET_LIST,
payload: {
list,
serviceName,
},
});
};
}
removeItem = (index) => {
return (dispatch) => {
dispatch({
type: this.REMOVE_ITEM,
payload: {
index,
},
});
};
}
reducer = (state = initialState, action) => {
switch (action.type) {
case this.GET_LIST:
return getListReducer(state, action);
case this.REMOVE_ITEM:
return removeItemReducer(state, action);
default:
return state;
}
}
}
More information read there.