Let's say there is a tree in the state of my react-redux app and tree belongs to myReducer. In many cases I need this tree flattened, so I've got a selector:
const getTree = state => state.myReducer.tree;
export const getFlatNodes = createSelector(
[getTree],
(tree) => flattenTree(tree)
);
And now I need to access the flattened tree in an action
import { getFlatNodes } from 'path/to/selectors';
function myReducer(state = defaultState, action) {
switch (action.type) {
case SOME_ACTION:
const flattenedTree = getFlatNodes(state);
return {
...state,
smth: getSmthFromFlattenedTree(flattenedTree)
};
}
}
This code will not work as expected since state in the reducer is just a slice of the app state. A simple workaround I came up with is to wrap state to make the passed parameter compatible:
import { getFlatNodes as _getFlatNodes } from 'path/to/selectors';
const getFlatNodes = myReducer => _getFlatNodes({ myReducer });
The code works, although it looks quite hacky and I'm not sure if it won't cause any issues.
Does anybody have any idea why and how it can be done better or my approach is good enough already?
Maybe if you make that in your action
const someAction = () => (dispatch, getState) => ({
type: 'SOME_ACTION',
payload: getFlatNodes(getState())
// payload: getSmthFromFlattenedTree(getFlatNodes(getState()))
});
then in your reducer
function myReducer(state = defaultState, action) {
switch (action.type) {
case SOME_ACTION:
return {
...state,
smth: getSmthFromFlattenedTree(action.payload)
};
}
}
Related
I am trying to implement Redux on a React Hooks project, but it doesnt seems to work good. Am I doing something wrong here?
reducer.js
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS: {
state.educations = action.payload;
return state;
}
default:
return state;
}
}
action.js
import * as types from '../constans/home';
export const getEducations = () => {
return dispatch => {
const edus = [
{value: 1, name: 'Bachelor'},
{value: 2, name: "Master"}
]
dispatch({
type: types.GET_EDUCATIONS,
payload: edus
})
}
}
component
import React, {useEffect} from 'react';
import {connect} from 'react-redux';
import {getEducations} from '../../redux/actions/home';
function Header({educations, getEducations}) {
useEffect(() => {
getEducations(); //calling getEducations()
}, [])
useEffect(() => {
console.log(educations) //console educations after every change
})
return (
<div className="main-header">
</div>
)
}
const mapStateToProps = (state) => {
return {
educations: state.home.educations
}
}
const mapDispatchToProps = (dispatch) => {
return {
getEducations: () => { dispatch(getEducations())}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
And the education property in Header function is always an empty array, as in initialState.
While when I check on browser with Redux Devtools, it shows that the state contains those two object in array.
So no matter if I change the redux state or not, the properties of the component are going to stay as initialState.
In redux, you should avoid directly mutating the state of your reducer. Refrain from doing something like state.reducers = blah. In order for redux to know that you are trying to make an update to state, you need to return a completely new state object. Following these principles, your reducers will update correctly and your components will get the new data.
Reducer.js
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS: {
return {
...state,
educations: action.payload
};
}
default:
return state;
}
}
In the code above, we return a new state object. It will include everything from the existing state, hence ...state, and we just update the educations property with the action.payload.
Can try with the reducer written this way :
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS:
return {
...state, educations:action.payload
}
default:
return state;
}
}
It looks like you’re mutating the state in the reducer. The reducer should always return a new state object if something updated.
You could do what the answers above suggest, but i would recommend using a package like immer (https://www.npmjs.com/package/immer) or immutable.js to prevent any bugs down the line. Using the spread syntax can be dangerous if your state object has some deeply nested properties, and it’s hard to be 100% sure that you haven’t accidentally mutated something, especially as your app grows in size.
It looks like you have solved this while I was getting this typed up - I decided to post it regardless, as it may be helpful.
On top of what Christopher Ngo already mentioned, the following example outlines how you can interact with your store to create new educations and then view them, in separate components..
Cheers!
I encounter this all the time and resolved it with CLEAR then GET/SET state. This ensures a reset of the state call.
Reducers.js
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS: {
return {
...state,
educations: action.payload
};
}
case CLEAR_EDUCATIONS: {
return initialState;
}
default:
return state;
}
}
Hooks.js
...
const clearEducation = () => {
dispatch({ type: CLEAR_EDUCATION });
}
const getEducations = (payload) => {
clearEducation(); // this clearing of the state is key fire re-render
dispatch({ type: GET_EDUCATIONS, payload });
};
}
I'm using the Redux Thunk example template. When I dispatch an action in getInitialProps, that populates my store, the data is loaded but after the page is rendered, the store is still empty.
static async getInitialProps({ reduxStore }) {
await reduxStore.dispatch(fetchCategories())
const categories = reduxStore.getState().programm.categories;
console.log('STATE!!!', categories)
return { categories }
}
The categories will load correctly but when I inspect my store, the categories state is empty.
Here is my store:
import db from '../../api/db'
// TYPES
export const actionTypes = {
FETCH_PROGRAMMS: 'FETCH_PROGRAMMS',
FETCH_CATEGORIES: 'FETCH_CATEGORIES'
}
// ACTIONS
export const fetchCategories = () => async dispatch => {
const categories = await db.fetchCategories();
console.log('loaded Cate', categories)
return dispatch({
type: actionTypes.FETCH_CATEGORIES,
payload: categories
})
}
// REDUCERS
const initialState = {
programms: [],
categories: []
}
export const programmReducers = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_PROGRAMMS:
return Object.assign({}, state, {
programms: action.payload
})
case actionTypes.FETCH_CATEGORIES:
console.log('Payload!', action);
return Object.assign({}, state, {
categories: action.payload
})
default:
return state
}
}
How can I make the redux state loaded on the server (in getInitialProps) be carried over to the client?
After hours of searching for solution it seems like I found my problem. It seems like I need to pass an initialState when creating the store. So instead of this:
export function initializeStore() {
return createStore(
rootReducers,
composeWithDevTools(applyMiddleware(...middleware))
)
}
I'm doing this and it works now
const exampleInitialState = {}
export function initializeStore(initialState = exampleInitialState) {
return createStore(
rootReducers,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
)
}
If you do this:
return { categories }
in getInitialProps, categories should be available in component's props in client side.
It should be available in Redux as well, this could cause the problem:
return Object.assign({}, state, {
categories: action.payload
})
Take a look at this Object.assign, the function only takes 2 parameters.
My normal way of doing this:
return {
...state,
categories: action.payload,
};
I'm new to React Redux.
It's fantasic so that
I want to see about tasks state of Store.
but don't see it.
result
tasks: [undefined]
Ask
so how can I?
here code:
import { createStore } from 'redux'
const initalState = {
tasks: []
}
function tasksReducer(state = initalState, action) {
switch (action.type) {
case 'ADD_TASK':
return {
...state,
tasks: state.tasks.concat([action.task])
};
default:
return state;
}
}
const addTask = (task) => ({
type: 'ADD_TASK',
payload: {
task
}
})
const store = createStore(tasksReducer)
store.dispatch(addTask('like it'))
console.log(store.getState()) <-- here
The problem is your action. It's adding a task with a key of payload, but you're trying to concat it with action.task (Which doesn't exist).
The object that would be sent into your reducer would look like this:
{
type: 'ADD_TASK',
payload: {
task: 'like it'
}
}
You can see clearly here, action.task doesn't exist, but action.payload.task does. Either change the object around, or modify it so you can access it at action.task:
const addTask = (task) => ({
type: 'ADD_TASK',
task
})
Or, modify your reducer:
function tasksReducer(state = initalState, action) {
switch (action.type) {
case 'ADD_TASK':
return {
...state,
tasks: state.tasks.concat([action.payload.task])
};
default:
return state;
}
}
In the future: a bit of debugging would have gone a long way here (And avoided this question altogether). A simple console.log(action) at the top of your reducer, would log the above object, and you could infer based on why it's trying to add undefined why it wouldn't work.
I'm trying to get the store from the reducer.
i saw the redux architecture is not supporting sharing between reducers.
but its really needed in my case.
const initState = {
schemas: [],
};
const myReducer = (state , action) => {
state = state || initState;
switch (action.type) {
case 'redux-form/CHANGE':
const schemaId = getStore().context.schema;
let modifier = state.schemas.get(schemaId);
modifier[action.key] = action.value;
return {
...state
};
}
};
my app reducers:
const iceApp = combineReducers({
form: formReducer,
context,
myReducer,
});
thanks ahead.
You can add reducer functions to any level of your state, consider:
const complexReducer = (state, action) {
const {context, myReducer} = state;
switch (action.type) {
case 'redux-form/CHANGE':
// do something with myReducer
return {context, myReducer};
default:
return state;
}
}
// a simple utility function to call reducers in sequence on the same state
function composeReducers(...reducers) {
return (state, action) => reducers.reduceRight((acc, reducer) => reducer(acc, action), state);
}
const iceApp = composeReducers(complexReducer, combineReducers({
form: formReducer,
context,
myReducer,
}));
This will apply the complexReducer to the whole state coming from the simple reducers.
A different approach is to access context in the action and pass it as payload of the action.
const changeAction = ... // the redux-form/change action
const changeActionWithContext = () => (dispatch, getState) => {
const state = getState();
const schemaId = state.context.schema;
dispatch(changeAction(schemaId));
}
I don't now redux-form so I don't know whether it's possible or not to add custom payload to redux-form actions.
I'm quite new to Redux and from what I understand, a reducer should be created for each type of object. E.g. for user interaction a user reducer should be created. My question is: How do you handle cases where you require the object for different purposes?
Scenario: Imagine having a user reducer which returns the current user. This user would be required in the entire application and needed for general controls on every page.
Now what happens when you need to load another user which is used for different purposes. E.g. profile page: loading a user to display information.
In this case there would be a conflict if the user reducer would be used. What would be the correct way to handle this in Redux? In case a different reducer would have to be created, what would be the naming convention for the new reducer?
First, you've mentioned:
a user reducer which loads the current user
I don't know if I got you correctly, but if this means you want to fetch (from an API, for example) the current user inside the reducer, this is a wrong approach.
Reducers are intended to be pure functions. You can call them with the same arguments multiple times and they will always return the same expected state.
Side effects like that should be handled by action creators, for example:
actions/user.js
export const FETCH_ME = 'FETCH_ME'
export const FETCH_ME_SUCCESS = 'FETCH_ME_SUCCESS'
// it's using redux-thunk (withExtraArgument: api) module to make an async action creator
export const fetchMe = () => (dispatch, getState, api) => {
dispatch({ type: FETCH_ME })
return api.get('/users/me').then(({ data }) => {
dispatch({ type: FETCH_ME_SUCCESS, data })
return data
})
}
Inside your reducer you can simple get the data and set a new state (note that if you send the action with the same data multiple times, the state will always be the same).
reducers/user.js
import { FETCH_ME, FETCH_ME_SUCCESS } from '../actions/user'
const initialState = {
item: null,
loading: false
}
export const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ME:
return {
...state,
loading: true
}
case FETCH_ME_SUCCESS:
return {
...state,
loading: false,
item: action.data
}
default:
return state
}
}
Now, for your scenario:
Now what happens when you need to load another user which is used for different purposes. E.g. profile page: loading a user to display information.
You will just write another action creator for that:
actions/user.js
export const FETCH_ME = 'FETCH_ME'
export const FETCH_ME_SUCCESS = 'FETCH_ME_SUCCESS'
export const FETCH_USER = 'FETCH_USER'
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'
export const fetchMe = () => (dispatch, getState, api) => {
dispatch({ type: FETCH_ME })
return api.get('/users/me').then(({ data }) => {
dispatch({ type: FETCH_ME_SUCCESS, data })
return data
})
}
export const fetchUser = (id) => (dispatch, getState, api) => {
dispatch({ type: FETCH_USER })
return api.get(`/users/${id}`).then(({ data }) => {
dispatch({ type: FETCH_USER_SUCCESS, data })
return data
})
}
Then you adapt your reducer to manage more sets:
reducers/user.js
import { combineReducers } from 'redux'
import { FETCH_ME, FETCH_ME_SUCCESS, FETCH_USER, FETCH_USER_SUCCESS } from '../actions/user'
const initialState = {
item: null,
loading: false
}
const meReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ME:
case FETCH_ME_SUCCESS:
return userReducer(state, action)
default:
return state
}
}
const activeReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER:
case FETCH_USER_SUCCESS:
return userReducer(state, action)
default:
return state
}
}
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER:
case FETCH_ME:
return {
...state,
loading: true
}
case FETCH_USER_SUCCESS:
case FETCH_ME_SUCCESS:
return {
...state,
loading: false,
item: action.data
}
default:
return state
}
}
export default combineReducers({
activeUser: activeReducer,
me: meReducer
})
Your final user state should be something like:
{
me: {
item: null,
loading: false
},
active: {
item: null,
loading: false
}
}