Update React when Data in Object in Store has changed - reactjs

I've found lots of similar problems, can't seem to sort my case
I have a component that won't re-render when data changes.
When MODE changes, which is a string, the entity re-renders and updates.
When hotspot.description changes, it won't update.
I can see the description has changed in the store, I can console log the changes all the way to this component.
However I just can't get this component to update when the description changes in hotspot.
Any clues!?
Connected
const mapStateToProps = (state) => {
return {
mode: state.admin.hotspot.mode,
hotspot: state.admin.hotspot.edit,
}
}
Pure
export default class HotspotRenderer extends React.Component {
constructor(props) {
super(props)
this.state = {
hotspot:props.hotspot,
mode:props.mode,
}
}
componentWillReceiveProps(nextProps) {
this.setState({
hotspot : nextProps.hotspot,
mode: nextProps.mode,
})
}
render() {
const {hotspot,mode} = this.state
const isEditingText = hotspot && mode === HOTSPOT_EDIT_MODE.TEXT
const html = hotspot != null ? ReactHtmlParser(draftToHtml(hotspot.description)) : null
return (
<div>
{
isEditingText &&
<Container>
<div className={`hotspot-renderer hotspot${hotspot.id} hotspot-text-default`}><div>{html}</div></div>
</Container>
}
</div>
)
}
}
admin.state.hotspot
const initialState = {
isDraggingNewHotspot: false,
edit:null,
mode:null,
}
export function hotspot(prevState=initialState, action) {
switch(action.type) {
case START_ADD_HOTSPOT: return { ...prevState, isDraggingNewHotspot: true }
case FINISH_ADD_HOTSPOT: return { ...prevState, isDraggingNewHotspot: false }
case ADD_HOTSPOT: return { ...prevState, mode: HOTSPOT_EDIT_MODE.DRAG}
case EDIT_HOTSPOT: return { ...prevState, edit: action.hotspot}
case FINISH_EDIT_HOTSPOT: return { ...prevState, edit: null}
case EDIT_HOTSPOT_MODE: return { ...prevState, mode: action.mode }
case UPDATE_HOTSPOT: return { ...prevState, edit : action.hotspot }
case GO_TO_EDIT_SCENE: return { ...prevState, edit :null,mode :null }
case UPDATE_SCENE_HOTSPOT_SUCCESS: return { ...prevState, edit: processUpdatedHotspot(prevState.edit,action.payload) }
default: return prevState
}
}
function processUpdatedHotspot(prev,update){
if(!prev)
return null
if(!prev.id)
prev.id = update.id
return prev
}
Here is where the description is edited
updateHotspotDescription(description){
let hotspot = this.state.hotspot
hotspot.description = description
hotspot.imageUpdateRequired = true
this.setState({hotspot : hotspot})
this.state.onUpdateHotspot(hotspot)
}
This is dispatched whenever text is changed, via a draft-js editor.
The state is updated with the changes, and another entity is aware of them.

You have to follow the Immutable pattern to update your value, even before passing it to redux (see updating nesting objects in the link).
So before sending hotspot.edit to your reducer be sure to update the nested description object following the immutable pattern like this :
updateHotspotDescription(description){
const hotspot = {
...this.state.hotspot,
description, // shorthand for description: description
imageUpdateRequired: true,
};
this.setState({ hotspot });
this.state.onUpdateHotspot(hotspot);
}

So you have to question yourself, are you sure your action it's being actually taken?
Any non-case in the switch statement will return the previous state, therefore It's normal that It won't re-render.
Some tips to follow to verify if your redux state it's being updated:
Make sure your constants are imported correctly in your actions and in your reducer
Make sure the triggered action it's being properly taken by the reducer
Log the next state in your reducer before your return it, so you can be sure that the next state is the correct one
Follow this steps and let me know if your problem persists

Related

How to prevent showing the same error multiple times with Redux?

I'm relatively new to React and Redux, but I have a basic understanding of how the actions+store+reducers work to enable one way dataflow.
One issue that I've run into as I write my actual app is how to display alerts for errors exactly once per unique error.
The solution that I've come up with works quite well, but I feel like it shouldn't be required, or that with my inexperience I might be missing something.
Essentially it boils down to this:
/* In the reducer: */
const initialState = {
someStateData: 'wow state',
error: undefined,
errorId: 0,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case SUCCESS:
return {
...state,
someStateData: action.data,
error: undefined,
}
case ERROR:
return {
...state,
error: action.error,
errorId: state.errorId + 1,
}
default:
return state
}
}
/* In the view component: */
class SomeComponent extends Component {
constructor(props) {
super(props)
this.state = {
lastErrorId: 0,
}
}
processHandlers() {
if (this.props.error &&
this.props.lastErrorId !== this.state.lastErrorId) {
this.props.onFailure?.(this.props.error)
this.setState({
...this.state,
lastErrorId: this.props.lastErrorId,
})
}
}
componentDidMount() {
this.processHandlers()
}
componentDidUpdate(prevProps, prevState) {
this.processHandlers()
}
}
const mapStateToProps = state => {
return {
error: state.error,
lastErrorId: state.errorId,
}
}
export default (connect(mapStateToProps)(SomeComponent))
This works great - is there another, better way? More idiomatic with Redux?
Add it to your logic or architecture...once the error occurs, save it, that could be another slice of your store that keeps track errors and other current issues. That is the point of state management, what is happening in your application right now...

Component props will not update after state if redux state changes

So I have read through numerous post which seem to cover the same topic as me, but after trying all their solutions, I simply cant make it work, so here it goes.
I have a simple search filter, that I use to filter some recipes by their name. I have a Redux Store to handle the state(value) of the search filter. When entering values in the input field, my actions goes fine to the reducer, which updates the state (I can see that with both console.logs and Redux DevTools). But somehow, this doesn't update my props, which are mapped with connect and mapStateToProps
All previous answers I have come across, points to mutating state, but I have tried to go over multiple iterations of my reducer, but then again, I might just be overlooking something.
My filter action dispatch:
filter(e){
Store.dispatch({
type: 'SET_NAME_FILTER',
payload: e.target.value
})
}
My reducer:
const filterNameReducer = (state = [""], action) => {
if (action.type === 'SET_NAME_FILTER') {
return [
action.payload
];
}
return state;
}
My RecipeListContainer:
class RecipeListContainer extends React.Component {
render() {
return (
<RecipeList recipes={ this.props.recipes } filterName={ this.props.filterName }/>
)
}
}
const mapStateToProps = (state) => {
return {
filterName: state.filterNameState,
}
}
module.exports = connect(mapStateToProps)(RecipeListContainer)
My RecipeList component:
class RecipeList extends React.Component {
render() {
return (
<ul>
{this.props.recipes.filter(this.filters, this).map(({ node: recipe }) => {
return (
<RecipeListItem key={ recipe.id } id={ recipe.id } value={ 0 } name={ recipe.name } time={ recipe.time } type={ recipe.type } tags={ recipe.tags } ingredients={ recipe.ingredients } />
);
})}
</ul>
)
}
filters() {
if (this.props.filterName[0].length != 0) {
return true;
} else {
return false;
}
}
}
I know that my filter doesn't do that much yet, but I've made it simple, so that i just could see if it gets updated. And it filter correct, if i change the initial value of my reducer. So shouldn't be that.
Also, Im using it in a GatsbyJS, but according to their documentation and examples, this should not be a problem. But i could be wrong?
Lots of thanks in advance! If I have missed something for you to find a solution, let me know, Im new to posting on SO.

setState works but redux store update doesn't

First, I want to mention that the only thing I'm changing between two approaches is setState vs going through the Redux store. Not changing anything else i.e. components, etc.
If I use the setState approach, I can close my modal but if I go through the store, it doesn't close. Any idea why?
Here's my reducer:
import 'babel-polyfill';
import * as types from '../actions/actionTypes';
const initialState = {
modals: {
"modal1": { isDisplayed: true },
"modal2": { isDisplayed: false }
}
};
export default (state = initialState, action) => {
switch (action.type) {
case types.SET_IS_DISPLAYED_MODAL :
return Object.assign({}, state, {
modals: action.modals
})
default: return state
}
}
}
Here are the two versions of my onClick action that is supposed to close the modal.
This is the setState version and it works:
displayModal(modalId, value)
{
let modals = this.props.modals;
modals[modalId].isDisplayed = value;
return setState({modals: modals});
}
And here's the version that goes through the redux store and it does NOT close my modal.
displayModal(modalId, value)
{
let modals = this.props.modals;
modals[modalId].isDisplayed = value;
return this.props.actions.displayModal(modals);
}
There's not much to the action but here it is:
export const displayModal = (modals) => {
return {
type: types.SET_IS_DISPLAYED_MODAL,
modals
};
}
Just so you see how it looks in my component, here it is:
render() {
return(
<div>
<div>Some info...</div>
{this.props.modals["modal1"].isDisplayed
? <Modal1 />
: null}
{this.props.modals["modal2"].isDisplayed
? <Modal2 />
: null}
</div>
);
}
BTW, I know that I'm hitting the action and the reducer. I also know that if I put a debugger in my mapStateToProps, I'm hitting it with updated state for my modals. So I know both the action and the reducer are doing what they're supposed to.
UPDATE:
I just tried something and this fixed the issue. I added last line to mapStateToProps and updated the section in my component:
function mapStateToProps(state) {
return {
modals: state.modals,
isModal1Displayed: state.modals["modal1"].isDisplayed // Just added this line
}
}
And changed the code in my component to:
render() {
return(
<div>
<div>Some info...</div>
{this.props.isModal1Displayed
? <Modal1 />
: null}
</div>
);
}
First of all, never mutate state in Redux reducer - it must be a pure function to work and detect changes correctly. Same rules apply to objects which you get with props.
You must change your code so you only dispatch an action to the store and reduce it to a new state.
First, dispatch an action:
displayModal(modalId, value)
{
this.props.actions.displayModal(modalId, value);
}
Your action will carry information which modal to hide or show:
export const displayModal = (modalId, value) => {
return {
type: types.SET_IS_DISPLAYED_MODAL,
modalId,
value
};
}
Then you can reduce it:
export default (state = initialState, action) => {
switch (action.type) {
case types.SET_IS_DISPLAYED_MODAL :
return Object.assign({}, state,
{
modals: Object.assign({}, state.modals,
{
[action.modalId]: { isDisplayed: action.value }
})
})
default: return state
}
}
As you can see there is a lot of boilerplate here now. With ES6 and ES7 you can rewrite your reducer with the object spread operator or you can use Immutable.js library, which will help you with setting properties deep in the hierarchy.
Reducer with object spread operator looks like this:
export default (state = initialState, action) => {
switch (action.type) {
case types.SET_IS_DISPLAYED_MODAL :
return {
...state,
modals: {
...state.modals,
[action.modalId]: { isDisplayed: action.value }
}
}
default: return state
}
}
You may ask yourself why your fix works. Let me explain.
You change a modal state when you dispatch an action to the Redux by mutating state in place modals[modalId].isDisplayed = value. After that the action is dispatched, reduced and mapToProps gets called again. There is probably reference check in connect higher order component and you have mutated the modal but not the modals object so it has the same reference = your component doesn't re-render. By adding a isModal1Displayed field you are actually disabling optimizations because there is boolean comparison, not a reference check and your component rerenders.
I hope it will help you with understanding Redux and it's principles.

React - Redux - Connect data fetched to Component

I am creating a React application and integrating Redux to it in order to manage the state and do network requests.
I followed the Todo tutorial and I am following the async example from the redux website, but I am stucked.
Here is my problem, I want, in my application, to fetch a user from a remote server. So the server send me a json array containing an object (maybe it's better if the server send me directly an object ? )
The json I obtain looks like that (I put only two fields but there are more in real) :
[{first_name: "Rick", "last_name": "Grimes"}]
Anyway I can fetch the data from the server but I can't inject user's data into my application, I hope you can help me but the most important is that I understand why it doesn't work.
Here are my several files :
I have two actions, one for the request and the other for the response:
actions/index.js
export const REQUEST_CONNECTED_USER = 'REQUEST_CONNECTED_USER';
export const RECEIVE_CONNECTED_USER = 'RECEIVE_CONNECTED_USER';
function requestConnectedUser(){
return {
type: REQUEST_CONNECTED_USER
}
}
function receiveConnectedUser(user){
return {
type: RECEIVE_CONNECTED_USER,
user:user,
receivedAt: Date.now()
}
}
export function fetchConnectedUser(){
return function(dispatch) {
dispatch(requestConnectedUser());
return fetch(`http://local.particeep.com:9000/fake-user`)
.then(response =>
response.json()
)
.then(json =>
dispatch(receiveConnectedUser(json))
)
}
}
reducer/index.js
import { REQUEST_CONNECTED_USER, RECEIVE_CONNECTED_USER } from '../actions
function connectedUser(state= {
}, action){
switch (action.type){
case REQUEST_CONNECTED_USER:
return Object.assign({}, state, {
isFetching: true
});
case RECEIVE_CONNECTED_USER:
return Object.assign({}, state, {
user: action.user,
isFetching: false
});
default:
return state;
}
}
And I have finally my container element, that I have called Profile.js
import React from 'react';
import { fetchConnectedUser } from '../actions';
import { connect } from 'react-redux';
class Profile extends React.Component{
constructor(props){
super(props);
}
componentDidMount() {
const { dispatch } = this.props;
dispatch(fetchConnectedUser());
}
render(){
const { user, isFetching} = this.props;
console.log("Props log :", this.props);
return (
<DashboardContent>
{isFetching &&
<div>
Is fetching
</div>
}
{!isFetching &&
<div>
Call component here and pass user data as props
</div>
}
</DashboardContent>
)
}
}
function mapStateToProps(state) {
const {isFetching, user: connectedUser } = connectedUser || { isFetching: true, user: []}
return {
isFetching,
user: state.connectedUser
}
}
export default connect(mapStateToProps)(Profile)
In the code above, I always have the Is Fetching paragraph being display, but even when I remove it, I cannot access to user data.
I think I have the beginning of something because when I console.log I can see my actions being dispatched and the data being added to the state, but I think I am missing something in the link communication with this data to the UI Component.
Am I on the good way or do I have a lot of things to refactor ? Any help would be very helpful !
Seeing as you are immediately fetching the data I allow myself to assume that isFetching may be true at beginning. Add an initial state to your reducer
state = { isFetching: true, user: null }
Then assuming you setup the reducer correctly:
function mapStateToProps(state) {
const {isFetching, user } = state.connectedUser
return {
isFetching,
user
}
}
Hope this works, feels clean-ish.

React, Redux: Update state and re-render children

The scenario I'm facing is I have an array of jobs stored as Redux state. I have a container which accesses the data with Redux connect and uses parameter from this.props.params to then find the appropriate Job to then hand down to it's children as a prop.
Within the Child Component the Job triggers an action which updates the job and then is merged and updates the jobs in the store. This seems to be working fine but when the container re-renders the child does not. I can see that is has something to do with the fact that I don't store job as props or state.
My question is what is the best (Redux)way to handle this?
So if I change JobShow to include jobs={jobs} it re-renders as jobs has updated. This seems to indicate that state is not mutating, I also can confirm that the container itself re-renders and even re-runs renderView() but doesn't re-render JobShow.
Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import JobShow from './job_show';
class JobContainer extends Component {
constructor(props){
super(props);
}
renderView(job, customer){
let viewParam = this.props.params.view;
switch(viewParam){
case "edit": return <JobEdit customer={customer} job={job}/>
case "notes": return <JobShow customer={customer} activeTab={"notes"} job={job}/>
case "quote": return <JobShow customer={customer} activeTab={"quote"} job={job}/>
case "invoice": return <JobShow customer={customer} activeTab={"invoice"} job={job}/>
default: return <JobShow customer={customer} activeTab={"notes"} job={job}/>
}
}
render() {
let { jobs, customers } = this.props;
if (jobs.length < 1 || customers.length < 1){
return <div>Loading...</div>;
}
let job = jobs.find(jobbie => jobbie.displayId == this.props.params.jobId);
let customer = customers.find(customer => customer._id.$oid == job.customers[0]);
return (
<div className="rhs">
{this.renderView(job, customer)}
</div>
);
}
}
JobContainer.contextTypes = {
router: React.PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
jobs: state.jobs.all,
customers: state.customers.all
};
}
export default connect(mapStateToProps, actions )(JobContainer);
Reducer Snippet:
import update from 'react-addons-update';
import _ from 'lodash';
export default function(state= INITIAL_STATE, action) {
switch (action.type) {
case FETCH_ALL_JOBS:
return { ...state, all: action.payload, fetched: true };
case UPDATE_JOB_STATUS:
let newStatusdata = action.payload;
let jobToUpdate = state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId);
jobToUpdate.status = newStatusdata.newStatus;
jobToUpdate.modifiedAt = newStatusdata.timeModified;
let updatedJobIndex = _.indexOf(state.all, state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId));
let updatedState = update(state.all, {$merge: {[updatedJobIndex]: jobToUpdate}});
return { ...state, all: updatedState}
Looks like you figured it out yourself, but you may find a slight refactoring improves the clarity of what you're doing. Here are some examples of how you might change the structure of your reducers using the object assign operation:
case ActionTypes.EDIT_TODO:
return Object.assign({},
state,
{ text: action.text }
)
From: https://github.com/mjw56/redux/blob/a8daa288d2beeefefcb88c577a7c0a86b0eb8340/examples/todomvc/reducers/todos.js
There's more in the official redux docs as well (redux.js.org/docs/recipes/UsingObjectSpreadOperator.html)
You may also find writing tests and using a package like deepFreeze useful, as it will tell throw an error if you accidentally mutate state without realizing.
The problem as pointed out in comments by #markerikson was that I was mutating state. Using the $set function from react-addons-update I successfully updated the content.
The new reducer snippet looks like this:
case UPDATE_JOB_STATUS:
let newStatusdata = action.payload;
let jobToUpdate = state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId);
let updatedJob = update(jobToUpdate, {status:{$set: newStatusdata.newStatus}})
updatedJob = update(updatedJob, {modifiedAt:{$set: newStatusdata.timeModified}})
let updatedJobIndex = _.indexOf(state.all, state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId));
let updatedState = update(state.all, {$merge: {[updatedJobIndex]: updatedJob}});
return { ...state, all: updatedState}

Resources