I have a ReactJs app which uses Redux to manage its store. My state is a complex json whose fileds can change. When i initiate the reducers, i have to specify the initial state else when accessing it before fetching the content from server, i get 'undefined' error. Following is the example,
//Test.jsx
class Test extends React.Component{
componentWillMount(){
fetchContent({
type: SET_CONTENT
});
}
render(){
return(
<div> {this.props.header} </div>
)
}
mapStateToProps(state){
return{
header: state.reducer1.a.b.c
}
}
}
export default (mapStateToProps)(Test);
//reducer1.js
export default function reducer1(state = {}, action = {}) {
switch (action.type) {
case SET_CONTENT:
return Object.assign({}, action.data);
default:
return state;
}
}
//reducer2.js
export default function reducer2(state = {}, action = {}) {
switch (action.type) {
case SET_SOMETHING_ELSE:
return Object.assign({}, action.data);
default:
return state;
}
}
//CombinedReducer.js
export default combinedReducers(Object.assign({}, {reducer1}, {reducer2}))
Now when the component initialises for the first time, state.reducer1.a.b.c throws undefined because the fetchContent() doesnt seems to be called at this point.
So my question is how do i solve this issue? Is specifying the initial state in reducer the only option? like the following,
//reducer1.js
export default function reducer1(state = { a: { b: { c: "somedata"}}}, action = {}) {
switch (action.type) {
case SET_CONTENT:
return Object.assign({}, action.data);
default:
return state;
}
}
You can do that, but you can also just update your mapState function to check for the existence of the first key.
function mapStateToProps(state){
return{
header: state.reducer1.a ? state.reducer1.a.b.c : <div>Loading..</div>
}
}
Also I assume you mean to have mapStateToProps outside of the class definition, as it's a function you pass to connect and not specific to the component:
class C {
...
}
function mapStateToProps (state) {}
connect(mapStateToProps)(C)
Lastly, are you missing dispatch in your fetchContent call? Is this using redux-thunk?
In most cases you shouldn't store all response data in reducers. Your state hierarchy shouldn't repeat hierarchy of JSON response.
It would me more simpler to store only c variable in reducer:
//reducer1.js
export default function reducer1(state = { c: undefined }, action) {
switch (action.type) {
case SET_CONTENT:
return Object.assign({}, state, { c: action.data.a.b.c });
default:
return state;
}
}
Also, you have some mistakes in your code:
mapStateToProps function should be defined outside of class
Instead of directly calling fetchContent function, you should pass it result to dispatch.
// Test.jsx
class Test extends React.Component{
componentWillMount(){
dispatch(fetchContent({
type: SET_CONTENT
}));
}
render() {
const { header } = this.props;
return header ? (
<div>{header}</div>
) : (
<div>Loading...</div>
);
}
}
function mapStateToProps(state) {
return {
header: state.reducer1.c
}
}
export default (mapStateToProps)(Test);
Related
Context
The goal is to have a component with a key name being react-rendered in App.js when I press a specific key, registered in another component. The information is being passed thorugh a redux managed state.
The problem
It's simple :
I'm updating my state in my redux reducer but even when duplicating it (I can see it thanks to the redux dev tool that allows me to watch my prevState and my nextState being different)
And the question is as simple :
Why my App.js component won't re-render even after connecting to and
duplicating my state ?
I think I made sure that my state was duplicated with the spreading operation and my redux dev tool display me a good state update without having my prevState and nextState duplicated. I looked through a lot of posts and found only people that forgot to duplicate their state in their reducers, which I did not.
So what's the problem here ??
DevTool Sample
Code
Here is the code, quite simple. The interesting piece is playSound and playedKeys:
App.js :
import React from 'react'
import './App.css';
import { connect } from 'react-redux';
import KeyComponent from './Components/Key'
import SoundPlayer from './Components/Sounds'
const mapStateToProps = (state) => ({
...state.soundReducer
})
class App extends React.Component {
constructor(props) {
super(props);
}
render(){
return (
<div>
{console.log(this.props)}
{
this.props.playedKeys.map(key =>{
<KeyComponent keyCode={key}> </KeyComponent>
})
}
<SoundPlayer></SoundPlayer>
</div>
);
}
}
export default connect(mapStateToProps)(App);
Reducer
export default (state = {allSounds:{},playedKeys:[]}, action) => {
switch (action.type) {
case 'ADD_SOUND':
return reduce_addSound({...state},action)
case 'PLAY_SOUND':
return reduce_playSound({...state,playedKeys : [...state.playedKeys]},action)
default:
return state
}
}
function reduce_addSound (state,action){
let i = 0
state.allSounds[action.payload.key] = { players : new Array(5).fill('').map(()=>(new Audio())) , reader : new FileReader()}
//load audioFile in audio player
state.allSounds[action.payload.key].reader.onload = function(e) {
state.allSounds[action.payload.key].players.forEach(player =>{
player.setAttribute('src', e.target.result);
player.load();
player.id = 'test'+e.target.result+ i++
})
}
state.allSounds[action.payload.key].reader.readAsDataURL(action.payload.input.files[0]);
return state
}
function reduce_playSound(state,action){
state.playedKey = action.payload.key;
if(!state.playedKeys.includes(state.playedKey))
state.playedKeys.push(action.payload.key);
return state
}
Action
export const addSound = (key, input,player) => (dispatch,getState) => {
dispatch({
type: 'ADD_SOUND',
payload: {key : key, input : input}
})
}
export const playSound = (key) => (dispatch,getState) => {
dispatch({
type: 'PLAY_SOUND',
payload: {key : key}
})
}
The component registering the keypresses
import React from 'react'
import { connect } from 'react-redux';
import { playSound } from '../../Actions/soundActions';
const mapStateToProps = (state) => ({
...state.soundReducer
})
const mapDispatchToProps = dispatch => ({
playSound: (keyCode) => dispatch(playSound(keyCode))
})
class SoundPlayer extends React.Component {
constructor(props) {
super(props);
}
componentDidMount () {
this.playSoundComponent = this.playSoundComponent.bind(this)
document.body.addEventListener('keypress', this.playSoundComponent);
}
keyCodePlayingIndex = {};
playSoundComponent(key){
if(this.props.allSounds.hasOwnProperty(key.code)){
if(!this.keyCodePlayingIndex.hasOwnProperty(key.code))
this.keyCodePlayingIndex[key.code] = 0
this.props.allSounds[key.code].players[this.keyCodePlayingIndex[key.code]].play()
this.keyCodePlayingIndex[key.code] = this.keyCodePlayingIndex[key.code] + 1 >= this.props.allSounds[key.code].players.length ? 0 : this.keyCodePlayingIndex[key.code] + 1
console.log(this.keyCodePlayingIndex[key.code])
}
this.props.playSound(key.code);
}
render(){
return <div>
<h1 >Played : {this.props.playedKey}</h1>
{Object.keys(this.keyCodePlayingIndex).map(key =>{
return <p>{key} : {this.keyCodePlayingIndex[key]}</p>
})}
</div>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SoundPlayer);
Issue
You are mutating your state object.
state.allSounds[action.payload.key] = ...
state.playedKey = action.payload.key;
Solution
Update your reducer functions to return new state objects, remembering to correctly shallow copy each level of depth that is being updated.
export default (state = { allSounds: {}, playedKeys: [] }, action) => {
switch (action.type) {
case 'ADD_SOUND':
return reduce_addSound({ ...state },action);
case 'PLAY_SOUND':
return reduce_playSound({ ...state, playedKeys: [...state.playedKeys] }, action);
default:
return state
}
}
function reduce_addSound (state, action) {
const newState = {
...state, // shallow copy existing state
allSounds: {
...state.allSounds, // shallow copy existing allSounds
[action.payload.key]: {
players: new Array(5).fill('').map(()=>(new Audio())),
reader: new FileReader(),
},
}
};
// load audioFile in audio player
newState.allSounds[action.payload.key].reader.onload = function(e) {
newState.allSounds[action.payload.key].players.forEach((player, i) => {
player.setAttribute('src', e.target.result);
player.load();
player.id = 'test' + e.target.result + i // <-- use index from forEach loop
})
}
newState.allSounds[action.payload.key]
.reader
.readAsDataURL(action.payload.input.files[0]);
return newState;
}
function reduce_playSound (state, action) {
const newState = {
...state,
playedKey: action.payload.key,
};
if(!newState.playedKeys.includes(newState.playedKey))
newState.playedKeys = [...newState.playedKeys, action.payload.key];
return newState
}
Okay I've got it, it's always the simplest stupidest thing that we don't check huh.
Clarification
So my state was properly duplicated with reduce_addSound({ ...state },action) and reduce_playSound({ ...state, playedKeys: [...state.playedKeys] and like I wrote in my question, that wasn't the issue !
Issue
As old as it can get, I wasn't returning a component in my render function.. :
in App.js :
render(){
return (
<div>
{
this.props.soundReducer.playedKeys.map(key =>{
<KeyComponent keyCode={key}> </KeyComponent> //<-- NO return or parenthesis !!
})
}
<SoundPlayer></SoundPlayer>
</div>
);
}
Answer
App.js render function with parenthesis:
render(){
return (
<div>
{
this.props.soundReducer.playedKeys.map(key =>(
<KeyComponent key = {key} keyCode={key}> </KeyComponent> //<-- Here a component is returned..
))
}
<SoundPlayer></SoundPlayer>
</div>
);
}
I set some values in React Redux store while the ComponentDidMount() function. Redux Dev Tools displays that the state has been updated. But in props It doesn't get changed.
My reducer.js is,
const inititalState = {
slickColumns: null,
slickData: null
}
const reducer = (state = inititalState, actions) => {
switch(actions.type) {
case actionsTypes.SET_SLICK_GRID_COLUMNS: {
return {
...state,
slickColumns: columns
};
}
case actionsTypes.SET_SLICK_GRID_DATA: {
return {
...state,
slickData: [...mock_slick_data]
};
}
default: {
return state;
}
}
}
export default reducer;
action.js,
import * as actions from './actions';
export const setSlickGridColumns = () => {
return {
type:actions.SET_SLICK_GRID_COLUMNS
}
}
export const setSlickGridData = () => {
return {
type: actions.SET_SLICK_GRID_DATA
}
}
main.js, (Mapping Redux to state)
const mapStateToProps = state => {
return {
slickColumns: state.slickColumns,
slickData: state.slickData
};
};
const mapDispatchToProps = dispatch => {
return {
onSetSlickDataColumns: () => {
dispatch(actionTypes.setSlickGridColumns());
},
onSetSlickData: () => {
dispatch(actionTypes.setSlickGridData());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Dimensions()(Home));
in ComponentDidMount function,
this.props.onSetSlickDataColumns(); // able to see this method is running and the state is updated in Redux Dev Tools
this.props.onSetSlickData();
console.log(this.props.slickColumns); // getting null here.
dv.setItems(this.props.slickData);
Even thought the state is updated in store, I am still not able to get the data in the props? why? any ideas?
index.js,
import slickReducer from './store/reducers/SlickGrid';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
slickReducer,
composeEnhancers(applyMiddleware(thunk))
);
const app = (
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
ReactDOM.render(app, document.getElementById("root"));
[Edit]: Initially I set initialState object properties as 'null;
Adding my Redux Screenshot here,
Adding extra some logs in here. This might helpful to resolve this issue. Actually the grid instance is created in ComponentDidMount() method.
componentDidMount() {
console.log("[componentDidmount]....");
this.props.onSetSlickDataColumns();
this.props.onSetSlickData();
console.log(this.props);
if (this.props.slickColumns.length !== 0) {
const grid = (this.gridInstance = new Grid(
this.grid,
dv,
this.props.slickColumns,
options
));
// ------------------
}
}
When doing grid object, It should not have empty columns, and empty DataView.
I called the setSlickGridColumns and setSlickGridData method in various lifecycle methods such as constructor, componentWillMount and put console logs for props in mapStateToProps, constructor, componentWillMount, render and componentDidMount methods also. From the logs what I am getting is,
[mapStateToProps]
this.props.slickColumns: null
[componentConstructor].... calling onSetSlickDataColumns() and onSetSlickData() methods here..
this.props.slickColumns: null
[componentwillmount].... calling onSetSlickDataColumns() and onSetSlickData() methods here..
this.props.slickColumns: null
[render]
this.props.slickColumns: null
[componentDidmount].... calling onSetSlickDataColumns() and onSetSlickData() methods here..
this.props.slickColumns: null
[mapStateToProps]
this.props.slickColumns: Array(300) // Here props values are set
[render]
this.props.slickColumns: Array(300)
From the logs, what I understand is, The data has to be filled before the componentDidMount() method. But It doesn't setting up even though I dispatched reducer function in constructor and ComponentWillMount. Hope this logs help to resolve this problem.
Problem is you are not setting new data in your reducer you can see
const reducer = (state = inititalState, actions) => {
switch(actions.type) {
case actionsTypes.SET_SLICK_GRID_COLUMNS: {
return {
...state,
slickColumns : columns // you have to pass your new data as payload from your function : actions.payload
};
}
case actionsTypes.SET_SLICK_GRID_DATA: {
return {
...state,
slickData: [...mock_slick_data] // same here
};
}
default: {
return state;
}
}
}
You can pass your data when you dispatch an action
dispatch(actionTypes.setSlickGridColumns('Pass your data here '));
Then you can get your data as argument like
export const setSlickGridColumns = (data) => {
return {
type:actions.SET_SLICK_GRID_COLUMNS,
payload : data // pass your data as payload
}
}
Now you can use your data in reducer like actions.payload
.......
case actionsTypes.SET_SLICK_GRID_COLUMNS: {
return {
...state,
slickColumns : action.payload
};
........
try below code -> you need to return the dispatch inside the mapDispatchToProps like below
const mapDispatchToProps = dispatch => {
return {
onSetSlickDataColumns: () => {
return dispatch(actionTypes.setSlickGridColumns());
},
onSetSlickData: () => {
return dispatch(actionTypes.setSlickGridData());
}
};
};
Actually you're not going to get updated props in componentDidMount lifecycle hook, as you are dispatching your function after your component is mounted. You'll get updated props in your render, componentWillReceiveProps (deprecated), getDerivedStateFromProps and some other lifecycle hooks. You can read more about which lifecycle hooks are being called when props are updated in the official docs of react.
And then you're missing return statement in mapDispatchToProps as mentioned in one other answer.
I have a component that makes an API call and then updates the state through a reducer. The problem is, this doesn't work so well cause the data don't get updated in the component, it's like the react didn't notice a state change a never re-rendered the component, but I'm not sure if that's the real issue here. So the component looks like this:
class MyComponent extends Component {
componentDidMount() {
// ajax call
this.props.loadData(1);
}
render() {
return (
<Grid>
<MySecondComponent
currentData={this.props.currentData}
/>
</Grid>
);
}
}
const mapStateToProps = state => ({
reducer state.myReducer,
currentData: state.myReducer.currentData
});
const mapDispatchToProps = dispatch => {
return {
loadData: () => {
HttpClient.getData(id, (data) => {
dispatch(
action_loadCurrentData(
data
)
);
});
},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
I am doing 2 things here: issuing an API call as soon as component is mounted, and then after data is fetched, dispatching action_loadCurrentData
This action looks like this:
//Action
export function action_loadCurrentData(
data
) {
return {
type: 'LOAD_CURRENT_DATA',
payload: {
currentData: data,
}
};
}
and the reducer:
//Reducer
const defaultState = {
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'LOAD_CURRENT_DATA':
state = {
...state,
currentData: {
myData: {
...state.currentData.myData,
0: action.payload.currentData
}
}
};
}
};
export default myReducer;
So the issue here is that the this.props.currentData that I'm passing to MySecondComponent will end up empty, as if I didn't set the data at all. However, If I stop the execution in the debugger and give it a few seconds, the data will be populated correctly, so I'm not sure what I'm doing wrong here?
Don't reassign state, return the newly created object instead
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'LOAD_CURRENT_DATA':
return {
...state,
currentData: {
myData: {
...state.currentData.myData,
0: action.payload.currentData
}
}
};
}
};
Your reducer needs to return the new state object, which needs to be a different instance from the previous state to trigger components update.
According to redux documentation:
The reducer is a pure function that takes the previous state and an action, and returns the next state.
And
Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
I'm fairly new to redux & thunk, and have been following tutorials to try and understand, and am managing to work it into my app ok. One thing i'm not understanding, is how i can get several state objects on the root level into one nested object. For example, right now my state looks like:
{
timeline: [Array] // My timeline data in an array of objects
timelineHasErrored: false,
timelineIsLoading: false
}
But what I really want is:
{
timeline : {
data: [Array] // My timeline data in an array of objects
hasErrored: false,
isLoading: false
}
}
and i'm really not quite sure how to nest these, or what the proper way to do that is. Below is my redux code, it's pretty simple so i'll post it all.
Reducers index
import { combineReducers } from 'redux'
import { timeline, timelineHasErrored, timelineIsLoading } from './timeline'
export default combineReducers({
timeline, timelineHasErrored, timelineIsLoading
});
Timeline Reducers
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
export function timelineHasErrored(state = false, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return action.hasErrored;
default:
return state;
}
}
export function timelineIsLoading(state = false, action) {
switch (action.type) {
case TIMELINE_IS_LOADING:
return action.isLoading;
default:
return state;
}
}
export function timeline(state = [], action) {
switch (action.type) {
case TIMELINE_FETCH_DATA_SUCCESS:
return action.timeline;
default:
return state;
}
}
Actions
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
import api from '../services/api'
export function timelineHasErrored(bool) {
return {
type : TIMELINE_HAS_ERRORED,
hasErrored : bool
}
}
export function timelineIsLoading(bool) {
return {
type : TIMELINE_IS_LOADING,
isLoading : bool
}
}
export function timelineFetchDataSuccess(timeline) {
return {
type : TIMELINE_FETCH_DATA_SUCCESS,
timeline
}
}
export function timelineFetchData() {
return dispatch => {
dispatch( timelineIsLoading(true) )
api.getTracks().then(
res => {
dispatch( timelineIsLoading(false) )
dispatch( timelineFetchDataSuccess(res.body) )
},
err => {
dispatch( timelineIsLoading(false) )
dispatch( timelineHasErrored(true) )
}
)
}
}
And then in my react component I format the object like how i want it... but i think it would be better to have it nested in the actual state so i'm not creating extra work for myself if things change
// Redux State
const mapStateToProps = (state) => {
const obj = {
timeline : {
data : state.timeline,
hasErrored: state.tracksHasErrored,
isLoading: state.tracksIsLoading
}
}
return obj
}
// Redux Dispatch
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch( timelineFetchData() )
}
}
If anybody has any tips or corrections for me bring em on, i'm trying to get a solid grasp on redux, thanks!
Your timeline reducer is pretty small, so you could have it as a single reducer as follows:
const initialState = {
data: [],
hasErrored: false,
isLoading: false
};
export function timeline(state = initialState, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return {
...state,
hasErrored: action.hasErrored
};
case TIMELINE_IS_LOADING:
return {
...state,
isLoading: action.isLoading
};
case TIMELINE_FETCH_DATA_SUCCESS:
return {
...state,
data: action.timeline
};
default:
return state;
}
}
Then you wouldn't need to call combineReducers(), unless you had other reducers.
I've taken two courses, treehouse and one on udemy, on react/redux and just when I think to myself "hey you got this, let's do a little practice" I run into some huge bug I can't seem to diagnose.
What I'm trying to do here sounds very simple, and in plain javascript it works. My state is an empty object state = {} and when my action is called, it creates an array inside of state noteName. So at the end of the day state should look like state = { noteName: [ ...state.noteName, action.payload]}.
When I console.log(this.props.inputvalue) it will return whatever is in the input element. I thought I understood objects because that consolelog should return the array noteName and not the actual value, correct?
Code
actions/index.js
export const INPUT_VALUE = 'INPUT_VALUE';
export function addNoteAction(text) {
return {
type: INPUT_VALUE,
payload: text
}
}
reducers/reducer_inputvalue.js
import { INPUT_VALUE } from '../actions';
// state is initialized as an empty object here
export default function(state = {}, action) {
switch (action.type) {
case INPUT_VALUE:
state.noteName = [];
// this SHOULD create an array that concats action.payload with
// whatever is already inside of state.name
return state.noteName = [...state.noteName, action.payload];
default:
return state;
}
}
noteitems.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class NoteItems extends Component {
render() {
return (
<ul>
{
this.props.inputvalue.noteName
?
this.props.inputvalue.noteName.map((note, index) => {
// this should iterate through noteName but returns undefined
return <li key={index}>{note}</li>;
})
:
<li>Nothing here</li>
}
</ul>
);
}
}
function mapStateToProps(state) {
return {
inputvalue: state.inputvalue
}
}
export default connect(mapStateToProps)(NoteItems);
This is happening because every time the action INPUT_VALUE is dispatched, you are resetting noteName. The main principle of redux is to not modify the state, but creating a new one based on the current. In your case:
const initialState = {
noteName: []
};
export default function(state = initialState, action) {
switch (action.type) {
case INPUT_VALUE: return {
noteName: [...state.noteName, action.payload]
};
default: return state;
}
}
You are overwriting state.noteName in the first line of your switch case.
switch (action.type) {
case INPUT_VALUE:
state.noteName = [];
In Redux, the point is to never overwrite a value, but to return a new value that might be a brand-new value, might be a value that is based on the old value (but still a new value... not overwriting the old), or it might be returning the old value (completely unmodified).
const counterReducer = (counter, action) => {
const options = {
[COUNTER_INCREMENT]: (counter, action) =>
({ value: counter.value + 1 }),
[COUNTER_DECREMENT]: (counter, action) =>
({ value: counter.value - 1 }),
[COUNTER_RESET]: (counter, action) =>
({ value: 0 })
};
const strategy = options[action.type];
return strategy ? strategy(counter, action) : counter;
};
At no point in that example am I modifying a value on counter. Everything is treated as read-only.