React, updating local states when redux reducer is updated - reactjs

I am making a project using React and Redux. I am updating a reducer with an array and then mapping that array into a component. The component has a state which is used to conditionally render a an image. I want this components state to be reset to its default whenever the reducers state is updated.
Here is the reducer :
const reducer = (state={board : []}, action) => {
switch (action.type) {
case 'MAKE_BOARD':
return Object.assign({}, state, {
board: action.payload
})
default:
return state
}
}
Here is the App.js page which calls the reducer:
import React, { Component } from 'react';
import './App.css';
import Board from '../Board/Board'
import {connect} from 'react-redux'
const mapReduxStateToProps= (reduxState) => ({
reduxState
})
class App extends Component {
state = {
size : '8',
squareArray : []
}
handleChange =(event) => {
this.setState({
...this.state,
size : Number(event.target.value)
})
console.log(this.state)
}
//This function makes a an array of numbers with 1/4 'X's and 3/4 'O's
boardMaker = (number) => {
this.setState({squareArray:[]});
let size = number*number;
let placeHolderArray = []
for(let i=0; i<size; i++){
placeHolderArray.push('O')
}
for(let j=0; j<size/4;j++){
placeHolderArray[Math.floor(Math.random()*size)] = 'X'
}
this.setState({squareArray: placeHolderArray})
console.log(placeHolderArray)
console.log(this.state.squareArray);
this.props.dispatch({type:'MAKE_BOARD', payload: placeHolderArray})
this.props.dispatch({type: 'SET_SIZE', payload : this.state.size})
}
render() {
return (
<div className="App">
<header className="App-header">
<input onChange={this.handleChange} placeholder='Size'/>
<button onClick={()=>this.boardMaker(this.state.size)}>Make Board</button>
<div className='board' style={{width: 40*this.props.reduxState.size.size}}>
{/* {this.state.squareArray.map(space => {
return(
<div className='square'>{space}</div>
)
})} */}
{JSON.stringify(this.props.reduxState)}
{this.props.reduxState.reducer.board.map((space,index) =>
<Board keys={index} id={space}/>
)
}
</div>
</header>
</div>
);
}
}
export default connect(mapReduxStateToProps)(App);
Here is the board.js where the reducer is being mapped:
import React, { Component } from 'react';
import './Board.css'
import { connect } from 'react-redux';
const mapReduxStateToProps = (reduxState) => ({reduxState})
class Board extends Component {
state = {
clicked: false,
displayFlag: false,
counter: 0,
}
imageDisplay= () => {
if(!this.state.clicked && !this.state.displayFlag){
return <img key={this.props.id} src='images/Frog-1.png' alt='Not Clicked'/>
} else if(this.state.displayFlag){
return <img src='images/Yellow.png' alt='None' />
} else {
return this.state.counter;
}
}
handleMouseDown = e => {
document.oncontextmenu = function() {
return false;
}
e = e || window.event;
//console.log(e.which)
console.log(this.state)
switch(e.which) {
case 1 : this.showNumber(); break;
case 2 : break;
case 3 : this.displayFlag(); return false;
default: break;
}
}
displayFlag= () => {
console.log('running')
this.setState({...this.state, displayFlag : !this.state.displayFlag })
return this.state.displayFlag;
}
showNumber= () => {
console.log('run')
let Xcounter = 0;
let edge = Math.sqrt(this.props.reduxState.reducer.board.length)
console.log(edge)
let keys = this.props.keys
let board = this.props.reduxState.reducer.board
let minX = keys%edge === 0 ? 0 : -1;
let maxX = keys%edge === (edge-1) ? 0 : 1;
let minY = Math.floor(keys/edge) == 0 ? 0 : -1;
let maxY = Math.floor(keys/edge) == (edge-1) ? 0 : 1;
for(let x = minX; x <= maxX; x++){
for(let y = minY; y<=maxY; y++){
if(board[keys+x+(y*edge)]=== 'X'){
Xcounter++
}
}
}
if(this.props.id === 'X'){
this.setState({...this.state, clicked: true, counter: 'X'})
return this.state.counter;
}
this.setState({...this.state, clicked: true, counter: Xcounter})
return this.state.counter;
}
render() {
return (
<div className="App">
<div onMouseDown={()=>this.handleMouseDown()} className='square'>{this.imageDisplay()}</div>
</div>
);
}
}
export default connect(mapReduxStateToProps)(Board);
I want the local state on Board to reset when the reducer is updated. I can probably do this be adding properties to the reducer and using them in the component but I am hoping there is a better method.

Looking at your use case - you need to empty the local state on every MAKE_BOARD
action.
Here are two approaches that I had in mind -
You can check for the props that are getting updated and place a condition in your (depending on your react version) lifecycle methods such as componentWillReceiveProps / getDerivedStateFromProps / componentDidUpdate and empty your local state.
You can create a separate action something on the lines of RESET_ACTION_NAME which returns your initial state ( which is empty ) on every update. But for it to function correctly, you'll need to put your local state as an object in the redux store.
PS: If you know that your action to reset the state only has a limited scope, you needn't put your state in the store. But, if you know that this action spans across multiple components, you can consider transforming the local state into a redux store object.

This gets a bit into design opinion land, but I think the most straightforward way to deal with this is to put the board's state in redux as well rather than local state (probably in its own separate reducer in the same store), because (if I understand correctly) you're really saying that you want its state to change (reset) based on the 'MAKE_BOARD' action.
Another alternative would be to include an integer board id in your redux state that you increment each time you remake the board. Then if you use that as the “key” property of your board I believe that will cause it to automatically reset because it will unmount the previous board and create a new one.
If you want more implementation specifics about how to go about this, I recommend getting a version of the code into codepen or codesandbox and sharing that in your question as well (but it's good that you're including most of the relevant code directly in your question text).

Related

Where to keep reference to initial data for child component in ReactJS

I have a parent component which makes an API call after mounting. Part of the data it gets is display-initialization data for the child components, so I don't want to store it in its own state if it's not relevant to it. My issue is, where do i store the child data so that I can reference it and pass it to the child components' props in the parent's render method? I considered just having the child components fetch their own data after they mount, but it seems like it would be better to have at least some initial display, and then have them take care of updating themselves afterwards.
import React, { Component } from 'react';
import Game from './Game';
import GameSetup from './GameSetup';
let gameInit, setupInit;
class GameRoom extends Component {
id = this.props.match.params.id;
state = {};
boundHandlers = {
receiveGameRoomData: receiveGameRoomDataHandler.bind(this)
};
componentDidMount() {
this.props.socket.on('gameRoomData',
this.boundHandlers.receiveGameRoomData);
this.props.socket.send(JSON.stringify({event: 'getGameRoomData',
payload: this.id}));
}
componentWillUnmount() {
this.props.socket.off('gameRoomData',
this.boundHandlers.receiveGameRoomData);
}
render() {
const renderMainArea = () => {
if (this.state.gameInProgress) return <Game socket={this.props.socket}
initData={setupInit} />;
else if (this.state.gameInProgress === false) return <GameSetup
socket={this.props.socket} initData={gameInit} />;
return null; // this is only reached when gameInProgress is undefined,
meaning there's no data from the server yet
};
return (
<React.Fragment>
{renderMainArea()}
{this.state.scoreBoard ? <ScoreBoard scores={this.state.scoreBoard}
/> : null}
</React.Fragment>
)
}
}
function receiveGameRoomDataHandler(data) {
data = JSON.parse(data);
let inSetup = "setupData" in Object.keys(data);
if (inSetup) setupInit = data.setupData;
else gameInit = data.gameData;
inSetup ? delete data.setupData : delete data.gameData;
this.setState(data);
}
class ScoreBoard extends Component {
render() {
const scoreData = this.props.scores;
const playerNames = Object.getOwnPropertyNames(scoreData);
let tableData = '';
playerNames.forEach(name => {
tableData += `<tr><td>${name}<br/>${scoreData[name]}</td></tr>`;
});
return (
<table dangerouslySetInnerHTML={{__html: tableData}}></table>
)
}
}
export default GameRoom;

How to structure React code with Apollo Query so that a random queried array of objects doesn't rerender with state change?

The problem: I'm using Apollo Client, and have the deck rendered like this "/deck/2" and I want to randomly shuffle the cards, and display just one at a time with a button to view the next. I keep running in the problem with React re-rendering everytime the state is changed (my onClick index counter +1), which reshuffles the cards since the shuffledCards variable is inside the query. I'm not sure how to prevent this from happening.
How can I shuffle the list without worrying about them being reshuffled 'onClick' of the button. I imagine there is a way to get the randomized array outside of the render, which I can do in Regular react, but using Apollo queries I'm stumbling to understand.
This is where I am stumbling due to my inexperience in React and Graphql with Apollo, and I haven't found a similar Apollo graphql project to lean off of. I can't use map on an object, but maybe there is a way to use map to display 1 object of the array at a time? I haven't found a working solution.
What I intend to have happen: I simply want to render the shuffled array of cards one at a time, and pressing the next button should step through the cards in the randomized array, without re-rendering whenever I click the button, otherwise cards will be repeated at random.
Here's the code:
import React, { Component, Fragment } from "react";
```
import CardItem from "./CardItem";
const CARDS_QUERY = gql`
query CardsQuery($id: ID!) {
```
`;
export class Cards extends Component {
constructor(props) {
super(props);
this.state = {
index: 0
};
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
this.setState({
index: this.state.index + 1
});
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
render() {
let { id } = this.props.match.params;
id = parseInt(id);
return (
<Fragment>
<Query query={CARDS_QUERY} variables={{ id }}>
{({ data, error, loading }) => {
if (loading) {
return <Loading />;
}
if (error)
}
const CardsToRender = data.deck.cards;
//This variable gets reshuffled at every re-render
const shuffledCards = this.shuffle(CardsToRender);
//Single item to be returned
const item = shuffledCards[this.state.index];
if (this.state.index >= shuffledCards.length) {
return (
<div>
<h1>Finished</h1>
</div>
);
} else {
return (
<Fragment>
// Here I can get one item to display, but if I press next, the state changes which fires a re-render,
//shuffling the cards once more. My intention is to only shuffle cards at first render until the browser page is
//refreshed or user navigates away
<h1>{item.front}</h1>
<h1>{item.back}</h1>
//My second attempt is to map out the cards, but I have only been able to render a list,
// but not one at a time. Maybe there is a simple code solution in my .map to display
//one at a time without needing to change state?
{shuffledCards.map(card => (
<CardItem key={card.id} card={card} />
))}
<p>
<button onClick={this.goToNext}>Next</button>
</p>
</Fragment>
);
}
}}
</Query>
</Fragment>
);
}
}
```
I'll be grateful for any help provided. Thank you!
I am not aware of Appolo Query but the issue you mentioned is more related to React. You can try below one to avoid shuffling cards on each re-render.
Refactor your component into two parts.
1) ShuffleCards.js(give any name as you like :) ) - Move your component into "ShuffleCards" and pass shuffled cords to the child component where you can update the state to render the next card.
// ShuffledCards.js
import React, { Component, Fragment } from "react";
```
import CardItem from "./CardItem";
const CARDS_QUERY = gql`
query CardsQuery($id: ID!) {
```
`;
export class ShuffleCards extends Component {
constructor(props) {
super(props);
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
render() {
let { id } = this.props.match.params;
id = parseInt(id);
return (
<Fragment>
<Query query={CARDS_QUERY} variables={{ id }}>
{({ data, error, loading }) => {
if (loading) {
return <Loading />;
}
if (error) {
}
const CardsToRender = data.deck.cards;
const shuffledCards = this.shuffle(CardsToRender);
return (
<Cards shuffledCards={shuffledCards} />
);
}}
</Query>
</Fragment>
);
}
}
Move the code which handles the displaying card and updating the state to the "Cards" component.
import React, { Component, Fragment } from "react";
import CardItem from "./CardItem";
export class Cards extends Component {
constructor(props) {
super(props);
this.state = {
index: 0
};
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
this.setState({
index: this.state.index + 1
});
}
render() {
const {shuffledCards} = this.props || [];
return (
<div>
{
this.state.index >= shuffledCards.length ?
<div>
<h1>Finished</h1>
</div>
:
<Fragment>
<h1>{item.front}</h1>
<h1>{item.back}</h1>
{
shuffledCards.map(card => (
<CardItem key={card.id} card={card} />
))
}
<p>
<button onClick={this.goToNext}>Next</button>
</p>
</Fragment>
}
</div>
)
}
}
You are calling this.shuffle() in your render function - therefore it will shuffle on each render.
Move that to your constructor and it will only get called once.
constructor(props) {
super(props);
this.state = {
index: 0
};
this.goToNext = this.goToNext.bind(this);
const CardsToRender = data.deck.cards;
}

React get new data in componentDidUpdate

I have a component which receives a list of news from two or three APIs. the first time the component renders, the apis are called and data is rendered in componentDidMount
something like this:
componentDidMount() {
this.state.platforms.forEach((platform, i) => {
let objToSend = {
phrase: this.props.searchParams.phrase,
// this is the main place when input data changes
...this.props.searchParams.general_params,
...this.props.searchParams.platforms[i].api_params,
stringPath: platform,
apiPath: this.props.searchParams.platforms[i].apiPath,
}
this.props.loadData(objToSend)
// this is the api call using redux which sends data as props to this component
}
new when the phrase change, I want this component to re-render and re-run the componentDidMount, but it's not working because componentDidMount will run once.
So I used componentDidUpdate, but as there are a number of calls so the api is being updated continuesly.
How can I make the component re-render and re-run componentDidMount every time I change the phrase
You can use componentDidUpdate arguments (previousProps, previousState) to check whether some new changes happened.
Example
componentDidUpdate(previousProps, previousState) {
if (previousProps.phrase !== this.props.phrase) {
//Do whatever needs to happen!
}
}
I stopped the infinite loop for my situation this way.
Here's one way to do something() when you re-render.
import React, { Component } from 'react';
import { render } from 'react-dom';
const fakeFetch = (n) => {
console.log(`Doing fake fetch: ${n}`)
return n
}
class App extends Component {
state = {
value: false,
number: 0,
}
componentDidMount() {
const number = fakeFetch(this.state.number + 1);
this.setState({ number })
}
componentDidUpdate(prevProps, prevState) {
if (prevState.value !== this.state.value) {
const number = fakeFetch(this.state.number + 1);
this.setState({ number })
}
}
toggle = () => {
this.setState(({ value }) => ({ value: !value }))
}
render() {
return (
<React.Fragment>
<h1>Number: {this.state.number}</h1>
<button onClick={this.toggle}>re-render</button>
</React.Fragment>
);
}
}
render(<App />, document.getElementById('root'));
Live example here.

Update React when Data in Object in Store has changed

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

My Redux state has changed, why doesn't React trigger a re-render?

I am trying to design a notification component where notifications will appear on certain occasions (like connections problems, successful modifications, etc.).
I need the notifications to disappear after a couple of seconds, so I am triggering a state change to delete the notification from Redux state from setTimeout inside the notification's componentDidMount.
I can see that the state does change, but React-Redux is not re-rendering the parent component so the notification still appears on the DOM.
Here is my Redux reducer:
const initialState = {
notifications: []
}
export default function (state = initialState, action) {
switch(action.type) {
case CLEAR_SINGLE_NOTIFICATION:
return Object.assign ({}, state, {
notifications: deleteSingleNotification(state.notifications, action.payload)
})
case CLEAR_ALL_NOTIFICATIONS:
return Object.assign ({}, state, {
notifications: []
})
default:
return state
}
}
function deleteSingleNotification (notifications, notificationId) {
notifications.some (function (notification, index) {
return (notifications [index] ['id'] === notificationId) ?
!!(notifications.splice(index, 1)) :
false;
})
return notifications;
}
and my React components (Main and Notification):
/* MAIN.JS */
class Main extends Component {
renderDeletedVideoNotifications() {
console.log('rendering notifications');
const clearNotification = this.props.clearNotification;
return this.props.notifications.map((notification)=> {
return <Notification
key={notification.id}
message={notification.message}
style={notification.style}
clearNotification={clearNotification}
notificationId={notification.id}
/>
});
}
render() {
console.log('rerendering');
return (
<div className="_main">
<Navbar location={this.props.location} logStatus={this.props.logStatus}
logOut={this.logout.bind(this)}/>
<div className="_separator"></div>
{this.props.children}
<BottomStack>
{this.renderDeletedVideoNotifications()}
</BottomStack>
</div>
);
}
}
function mapStateToProps(state) {
return {logStatus: state.logStatus, notifications: state.notifications.notifications};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({checkLogStatus, logOut, clearNotification, clearAllNotifications}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Main);
/* NOTIFICATION.JS */
export default class Notification extends Component{
constructor(props){
super(props);
this.state = {show: true}
}
componentWillReceiveProps(nextProps){
if(nextProps.message){
this.setState({show: true});
}
}
clearNotification(notificationId){
this.props.clearNotifications(notificationId);
}
componentDidMount(){
console.log('notification mount');
setTimeout(()=>{
console.log('timed out');
this.props.clearNotification(this.props.notificationId);
}, 1000);
}
closeNotification(){
this.props.clearNotification(this.props.notificationId);
this.setState({show: false});
}
render(){
const notificationStyles = () =>{
if (this.props.style === "error"){
return {backgroundColor: 'rgba(152, 5, 19, 0.8)'}
}
return {backgroundColor: 'rgba(8, 130, 101, 0.8)'}
};
if(!this.state.show){
return null;
}
return (
<div className="notification" style={notificationStyles()}>
<div className="notificationCloseButton" onClick={this.closeNotification.bind(this)}>
<i className="material-icons">close</i>
</div>
{this.props.message}
</div>
)
}
};
You've got everything hooked up correctly, but you're missing one key concept for Redux:
With Redux, you never mutate any part of state.
From the Redux guide:
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().
In deleteSingleNotification, you're using .splice to cut the old notification out of your array. Instead, you need to return a brand new array with the unwanted notification missing from it. The easiest way to do this is with the .filter function:
function deleteSingleNotification(notifications, notificationId){
return notifications.filter (notification => {
return notification.id !== notificationId
}
}
Here is a JSBin with your working notification system!
So here is why this works: React-Redux's job is to update your components whenever a specific part of your Redux store is changed. It uses a === test on every part of the state tree to know if anything changed.
When you go and change the state with something like .splice, it checks and thinks nothing is different.
Here's an example to demonstrate the problem:
var array = [ 'a', 'b', 'c' ]
var oldArray = array
array.splice (1, 1) // cut out 'b'
oldArray === array // => true! Both arrays were changed by using .splice,
// so React-Redux *doesn't* update anything
Instead, React-Redux needs us to do this:
var array = [ 'a', 'b', 'c' ]
var oldArray = array
array = array.filter (item, index => index !== 1) // new array without 'b'
oldArray === array // false. That part of your state has changed, so your
// componenet is re-rendered
Redux uses this approach for performance reasons. It takes a really long time to loop through a big state tree looking to see if everything is the same. When you keep your tree immutable, only a === test is needed and the process gets much easier.

Resources