I have a weird problem with React-Redux app. One of my components doesn't re-render on props change which is updated by action 'SET_WINNING_LETTERS'. github repo: https://github.com/samandera/hanged-man
setWord.js
const initialWordState = {
word: []
};
const setWinningLetters = (wordProps) => {
let {word, pressedKey} = wordProps;
for (let i = 0; i < word.length; i++) {
if (/^[a-zA-Z]$/.test(pressedKey) && word[i].letter.toUpperCase() == pressedKey) {
word[i].visible = true;
}
}
return {word};
}
const setWord = (state = initialWordState, action) => {
switch(action.type) {
case 'SET_WORD': return Object.assign({}, state, getWord(action.word));
case 'SET_WINNING_LETTERS': return Object.assign({}, state,
updateWord(action.wordProps));
}
return state;
}
export default setWord;
In Index.js in this function the actions are triggered
handleKeyPress(pressedKey) {
store.dispatch({
lettersProps: {
word:this.props.word,
pressedKey,
missedLetters: this.props.missedLetters
},
type: 'SET_MISSED_LETTERS'
});
store.dispatch ({
wordProps: {
word:this.props.word,
pressedKey
},
type: 'SET_WINNING_LETTERS'
});
this.showEndGame(this.props.word,this.props.missedLetters);
};
componentWillMount() {
fetchWord(this.statics.maxWordLength);
window.onkeydown = () =>
{this.handleKeyPress(String.fromCharCode(event.keyCode))};
}
And in PrimaryContent.js Winning and Missing Characetrs are rendered
import React from 'react';
import {connect} from 'react-redux';
import store from '../reducers/store';
import Hangedman from './Hangedman';
import AspectRatio from './AspectRatio';
import Puzzle from './Puzzle';
import MissedCharacters from './MissedCharacters';
const mapStateToProps = (store) => {
return {
word: store.wordState.word,
missedLetters: store.missedLettersState.missedLetters
}
}
class PrimaryContent extends React.Component {
constructor() {
super();
}
renderDisabledPuzzles(amount){
return Array.from({length: amount}, (value, key) => <AspectRatio parentClass="disabled" />)
}
renderLetters(word) {
return word.map(function(letterObj, index) {
let space = (letterObj.letter==' ' ? "disabled": '')
return(
<AspectRatio parentClass={space} key={"letter" + index}>
<div id={"letter" + index}>{letterObj.visible ? letterObj.letter : ''}</div>
</AspectRatio>
)
}) ;
}
render() {
let disabledCount = this.props.puzzles - this.props.word.length;
let disabledPuzzles = this.renderDisabledPuzzles(disabledCount);
let WinningLetters = this.renderLetters(this.props.word);
return (
<div className="ratio-content primary-content">
<Hangedman/>
<MissedCharacters missedLetters={this.props.missedLetters}/>
<Puzzle>
{disabledPuzzles}
{WinningLetters}
</Puzzle>
</div>
);
}
}
export default connect(mapStateToProps)(PrimaryContent);
MissedCharacters works well while {WinningLetters} doesn't.
The action 'SET_MISSED_LETTERS' works perfect, while 'SET_WINNING_LETTERS' works only when 'SET_MISSED_LETTERS' gets updated. It means when I press one or more letter that wins they won't display until I press the letter that is missing. When I press the missing letter the component that is parent for both missing and winning letters re-renders. I was trying to pass props to PrimaryContent from it's parent but I get the same. I tried to separate {WinningLetters} in it's own component wit access to redux store but it works even worse and stops updating even when MissedCharacters updates. Can you detect where I've made a mistake?
Related
Basically, I have one component, let's call it component1 and a second component, which has been created by duplicating the first one called component2. I had to duplicate it, because some objects inside it had to be altered before sending them to the further components.
On one page I have an onClick event which triggers component1 which opens a modal and on another page, component2 is trigger the same as for the first one.
The problem occurs here, if I'm on the second page where the modal from component2 is opened and I refresh the page, both components are called, of course component1 is the first one called and the state is altered by this component which makes me not having the desired information in the second component.
As far as I understood, because of the fact that in both components, mapStateToProps is altering my state, both components are called. Not really sure though that I understood right.
Here is my component1 summary:
class LivePlayerModal extends React.Component {
constructor(props) {
super(props);
this.highlightsUpdated = null;
}
componentDidMount() {
const queryParam = UrlHelper.getParamFromLocation(IS_QUALIFICATION, window.location);
if (queryParam === null) {
ScoringLoader.subscribe(endpointNames.LIVE_SCANNER);
ScoringLoader.subscribe(endpointNames.PLAYERS);
ScoringLoader.subscribe(endpointNames.LEADERBOARD);
ScoringLoader.subscribe(endpointNames.COURSE);
ScoringLoader.subscribe(endpointNames.STATISTICS);
}
//TODO: make fixed fetch on timeout
this.fetchHighlights();
}
componentDidUpdate(prevProps) {
if (prevProps.playerId !== this.props.playerId) {
this.highlightsUpdated = null;
}
this.fetchHighlights();
}
componentWillUnmount() {
ScoringLoader.unsubscribe(endpointNames.LIVE_SCANNER);
ScoringLoader.unsubscribe(endpointNames.PLAYERS);
ScoringLoader.unsubscribe(endpointNames.LEADERBOARD);
ScoringLoader.unsubscribe(endpointNames.COURSE);
ScoringLoader.unsubscribe(endpointNames.STATISTICS);
}
render() {
const {
isOpen, scoringPlayer, isQualification, ...rest
} = this.props;
const highlightGroups = getHighlights(this.getCloudHighlights());
if (isQualification) {
return null;
}
return (
<ReactModal isOpen={isOpen} onCloseCb={this.hide}>
<div className="live-player">
{
scoringPlayer === undefined &&
<BlockPlaceholder minHeight={400}>
<BlockSpinner />
</BlockPlaceholder>
}
{
scoringPlayer === null &&
<LivePreMessage
model={{
title: '',
body: 'Player data coming soon'
}}
bemList={[bemClasses.LIGHT]}
/>
}
{
scoringPlayer &&
<LivePlayerLayout
{...rest}
scoringPlayer={scoringPlayer}
highlightGroups={highlightGroups}
/>
}
</div>
</ReactModal>
);
}
}
const mapStateToProps = (state, ownProps) => {
const isQualification = state.scoring.isQualification;
const { playerId } = ownProps;
const sitecorePlayers = state.scoring[endpointNames.PLAYERS];
const scoringLeaderboard = state.scoring[endpointNames.LEADERBOARD];
const getScoringPlayer = () => {
};
return ({
isQualification,
liveScanner: state.scoring[endpointNames.LIVE_SCANNER],
scoringLeaderboard,
scoringPlayer: getScoringPlayer(),
scoringStats: state.scoring[endpointNames.STATISTICS],
scoringCourse: state.scoring[endpointNames.COURSE],
sitecorePlayers: state.scoring[endpointNames.PLAYERS],
cloudMatrix: state.cloudMatrix
});
};
const mapDispatchToProps = (dispatch) => ({
fetchPlayerHighlights: (feedUrl) => dispatch(fetchFeed(feedUrl))
});
const LivePlayerCardContainer = connect(
mapStateToProps,
mapDispatchToProps
)(LivePlayerModal);
export default LivePlayerCardContainer;
Here is my component2 summary :
class QualificationLivePlayerModal extends React.Component {
constructor(props) {
super(props);
this.highlightsUpdated = null;
}
shouldComponentUpdate(nextProps) {
return nextProps.isQualification;
}
componentDidMount() {
ScoringLoader.subscribe(endpointNames.SUMMARY_FINAL);
ScoringLoader.subscribe(endpointNames.SUMMARY_REGIONAL);
ScoringLoader.subscribe(endpointNames.LIVE_SCANNER);
ScoringLoader.subscribe(endpointNames.PLAYERS);
ScoringLoader.subscribe(endpointNames.COURSE);
ScoringLoader.unsubscribe(endpointNames.LEADERBOARD);
ScoringLoader.unsubscribe(endpointNames.STATISTICS);
//TODO: make fixed fetch on timeout
this.fetchHighlights();
}
componentDidUpdate(prevProps) {
if (prevProps.playerId !== this.props.playerId) {
this.highlightsUpdated = null;
}
this.fetchHighlights();
}
componentWillUnmount() {
ScoringLoader.unsubscribe(endpointNames.SUMMARY_FINAL);
ScoringLoader.unsubscribe(endpointNames.SUMMARY_REGIONAL);
ScoringLoader.unsubscribe(endpointNames.COURSE);
ScoringLoader.unsubscribe(endpointNames.LEADERBOARD);
ScoringLoader.unsubscribe(endpointNames.STATISTICS);
}
render() {
const {
scoringPlayer, summaryFinal, ...rest
} = this.props;
const highlightGroups = getHighlights(this.getCloudHighlights());
const queryParam = UrlHelper.getParamFromLocation(IS_QUALIFICATION, window.location);
const open = (queryParam === 'true');
if (scoringPlayer !== undefined && scoringPlayer !== null) scoringPlayer.id = scoringPlayer.entryId;
return (
<ReactModal isOpen={open} onCloseCb={this.hide}>
<div className="qual-live-player">
{
scoringPlayer === undefined &&
<BlockPlaceholder minHeight={400}>
<BlockSpinner />
</BlockPlaceholder>
}
{
scoringPlayer === null &&
<LivePreMessage
model={{
title: '',
body: 'Player data coming soon'
}}
bemList={[bemClasses.LIGHT]}
/>
}
{
scoringPlayer &&
<LivePlayerLayout
{...rest}
scoringPlayer={scoringPlayer}
highlightGroups={highlightGroups}
/>
}
</div>
</ReactModal>
);
}
}
const mapStateToProps = (state, ownProps) => {
const isQualification = state.scoring.isQualification;
const { playerId, location } = ownProps;
const locationIdFromQueryParam = UrlHelper.getParamFromLocation(LOCATION_ID, window.location);
const locationId = location !== null ? location.locationId : locationIdFromQueryParam;
const sitecorePlayers = state.scoring[endpointNames.PLAYERS];
const summaryRegional = state.scoring[endpointNames.SUMMARY_REGIONAL];
const summaryFinal = state.scoring[endpointNames.SUMMARY_FINAL];
const scoringLeaderboard = getLeaderboardBasedOnLocation(locationId, summaryFinal, summaryRegional);
const currentRound = getCurrentRound(locationId, summaryFinal, summaryRegional);
const getScoringPlayer = () => {
};
return ({
isQualification,
liveScanner: state.scoring[endpointNames.LIVE_SCANNER],
scoringLeaderboard,
scoringPlayer: getScoringPlayer(),
scoringCourse: getScoringCourseFromQualificationFeed(),
sitecorePlayers: state.scoring[endpointNames.PLAYERS],
cloudMatrix: state.cloudMatrix,
});
};
const mapDispatchToProps = (dispatch) => ({
fetchPlayerHighlights: (feedUrl) => dispatch(fetchFeed(feedUrl))
});
const QualificationLivePlayerCardContainer = connect(
mapStateToProps,
mapDispatchToProps
)(QualificationLivePlayerModal);
export default QualificationLivePlayerCardContainer;
Basically, the problem i ve got here, is that in state.scoring I do not have the information for the endpoints present in the return statement of the render method before the page finishes the refresh process, which later on makes my app to break.
Hope I've been clear enough.
Is there a solution for waiting the endpoints to get called or even not loading the first component at all?
I have a React component that calls a reducer on initialisation to populate an array with as many blank records as a number specified in a firebase db. If the firebase db field myArray length is 5, the component will initialise and call the reducer to create 5 blank records in an array in the context:
import React, { useState, useEffect, useContext } from 'react';
import { useHistory, useLocation } from 'react-router';
import ArrayCard from '../../../containers/ArrayCard';
import firebase from '../../../Utils/firebase';
import MyContext from '../../../Utils/contexts/MyContext ';
import { initialiseMyArray, changeArray} from '../../../Utils/reducers/MyActions';
function MyComponent() {
const { push } = useHistory();
const location = useLocation();
const context = useContext(MyContext);
const dispatch = context.dispatch;
const session = location.state.currentSession;
const sessionRef = firebase.firestore().collection("sessions").doc(session);
const [ loaded, setLoaded ] = useState(false);
const [ myArray, setMyArray] = useState([]);
const [ myArrayCurrentIndex, setMyArrayCurrentIndex ] = useState();
const getSessionData = () => {
sessionRef.get().then(function(doc) {
if (doc.exists) {
const myArrayLength= parseInt(doc.data().myArrayLength);
dispatch({ type: initialisePensionsFromFirebase, myArrayLength: myArrayLength});
setMyArrayCurrentIndex (0);
setLoaded(true);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
}
useEffect(() => {
if (sessionRef && !loaded && myArray.length < 1) {
getSessionData();
}
if (context) {
console.log("CONTEXT ON PAGE: ", context)
}
}, [loaded, currentIndex])
The index currentIndex of the newly-created myArray in the context is used to populate a component inside this component:
return (
<innerComponent
handleAmendSomeproperty={(itemIndex, field, value) => handleAmendSomeproperty(itemIndex, field, value)}
itemIndex={currentIndex}
someproperty={context.state.someitem[currentIndex].someproperty}
></innerComponent>
)
I want to be able to amend the someproperty field in myArray[currentIndex], but my dispatch causes endless rerenders.
const handleAmendSomeproperty= (itemIndex, field, value) => {
dispatch({ type: changeItem, itemIndex: itemIndex});
}
My reducer switch cases are like so:
case initialiseMyArray:
console.log("SRSTATE: ", state);
let _initialArray = []
for (let i=0; i<action.myArrayLength; i++) {
_initialArray .push(
{
itemIndex: i,
someproperty: "" }
)
}
return {
...state,
someproperty: [..._initialArray ]
};
case changeArray:
// I want to leave the state untouched at this stage until I stop infinite rerenders
return {
...state
};
What is happening to cause infinite rerenders? How come I can't amend someproperty, but initialising the new array in the state works fine?
innerComponent has lots of versions of this:
<div>
<label>{label}</label>
<div onClick={() => handleAmendSomeproperty(itemIndex, "fieldName", "fieldValue")}>
{someproperty === "booleanValue" ? filled : empty}
</div>
</div>
and lots like this:
<input
type="text"
placeholder=""
value={somepropertyvalue}
onChange={(event) => handleAmendSomeproperty(itemIndex, "fieldName", event.target.value)}
/>
I'm currently fetching data in Component1, then dispatching an action to update the store with the response. The data can be seen in Component2 in this.props, but how can I render it when the response is returned? I need a way to reload the component when the data comes back.
Initially I had a series of functions run in componentDidMount but those are all executed before the data is returned to the Redux store from Component1. Is there some sort of async/await style between components?
class Component1 extends React.Component {
componentDidMount() {
this.retrieveData()
}
retrieveData = async () => {
let res = await axios.get('url')
updateParam(res.data) // Redux action creator
}
}
class Component2 extends React.Component {
componentDidMount() {
this.sortData()
}
sortData = props => {
const { param } = this.props
let result = param.sort((a,b) => a - b)
}
}
mapStateToProps = state => {
return { param: state.param }
}
connect(mapStateToProps)(Component2)
In Component2, this.props is undefined initially because the data has not yet returned. By the time it is returned, the component will not rerender despite this.props being populated with data.
Assuming updateParam action creator is correctly wrapped in call to dispatch in mapDispatchToProps in the connect HOC AND properly accessed from props in Component1, then I suggest checking/comparing props with previous props in componentDidUpdate and calling sortData if specifically the param prop value updated.
class Component2 extends React.Component {
componentDidMount() {
this.sortData()
}
componentDidUpdate(prevProps) {
const { param } = this.props;
if (prevProps.param !== param) { // <-- if param prop updated, sort
this.sortData();
}
}
sortData = () => {
const { param } = this.props
let result = param.sort((a, b) => a - b));
// do something with result
}
}
mapStateToProps = state => ({
param: state.param,
});
connect(mapStateToProps)(Component2);
EDIT
Given component code from repository
let appointmentDates: object = {};
class Appointments extends React.Component<ApptProps> {
componentDidUpdate(prevProps: any) {
if (prevProps.apptList !== this.props.apptList) {
appointmentDates = {};
this.setAppointmentDates();
this.sortAppointmentsByDate();
this.forceUpdate();
}
}
setAppointmentDates = () => {
const { date } = this.props;
for (let i = 0; i < 5; i++) {
const d = new Date(
new Date(date).setDate(new Date(date).getDate() + i)
);
let month = new Date(d).toLocaleString("default", {
month: "long"
});
let dateOfMonth = new Date(d).getDate();
let dayOfWeek = new Date(d).toLocaleString("default", {
weekday: "short"
});
// #ts-ignore
appointmentDates[dayOfWeek + ". " + month + " " + dateOfMonth] = [];
}
};
sortAppointmentsByDate = () => {
const { apptList } = this.props;
let dates: string[] = [];
dates = Object.keys(appointmentDates);
apptList.map((appt: AppointmentQuery) => {
return dates.map(date => {
if (
new Date(appt.appointmentTime).getDate().toString() ===
// #ts-ignore
date.match(/\d+/)[0]
) {
// #ts-ignore
appointmentDates[date].push(appt);
}
return null;
});
});
};
render() {
let list: any = appointmentDates;
return (
<section id="appointmentContainer">
{Object.keys(appointmentDates).map(date => {
return (
<div className="appointmentDateColumn" key={date}>
<span className="appointmentDate">{date}</span>
{list[date].map(
(apptInfo: AppointmentQuery, i: number) => {
return (
<AppointmentCard
key={i}
apptInfo={apptInfo}
/>
);
}
)}
</div>
);
})}
</section>
);
}
}
appointmentDates should really be a local component state object, then when you update it in a lifecycle function react will correctly rerender and you won't need to force anything. OR since you aren't doing anything other than computing formatted data to render, Appointments should just call setAppointmentDates and sortAppointmentsByDate in the render function.
This is quite peculiar because in my reducers, I made sure I was not mutating state; A common issue with this particular problem. However, I still keep getting this issue. On the initial load of the application (using npm start). In the photo below you can see that I console.log every component right before the return statement as a test to see if the components render. But despite state being updated, the component never re-renders.... (I'm confident the containers are set up properly and the components are called.)
AllGiftsDisplay
import OneGiftDisplay from './OneGiftDisplay.jsx';
const AllGiftsDisplay = (props) => {
console.log("LOADING");
let individualGifts = [];
for(let i = 0; i < props.giftList.length; i++) {
individualGifts.push(
<OneGiftDisplay
addGift = {props.addGift}
updatedGiftMessage = {props.updateGiftMessage}
setNewMessage = {props.setNewMessage}
totalVotes = {props.totalVotes}
/>
)
}
// let list = [<OneGiftDisplay/>, <OneGiftDisplay/>]
return (
<div className = "displayAllGifts">
{/* {console.log("~~~giftlist length", props.giftList.length)} */}
{individualGifts}
{/* {list} */}
</div>
)
};
export default AllGiftsDisplay;
Gift Reducers
import * as types from '../constants/actionTypes.js';
const initialState = {
giftList: [],
lastGiftId: 10000,
totalVotes: 0,
newMessage: ''
};
const giftReducer = (state=initialState, action) => {
// let giftList;
// let setMessage;
switch(action.type) {
case types.ADD_GIFT:
let stateCopy = { ...state }; // shallow copy
// create the new gift object structure.
const giftStructure = {
// lastGiftId: stateCopy.lastGiftId,
newMessage: stateCopy.newMessage,
totalVotes: 0
};
// push the new gift onto the list.
stateCopy.giftList.push(giftStructure);
// console.log("giftList: ", stateCopy.giftList);s
// return updated state
return {
...stateCopy,
newMessage: ''
}
case types.SET_MESSAGE:
return {
...state, newMessage: action.payload,
}
case types.ADD_VOTE:
case types.DELETE_GIFT:
default:
return state;
}
};
export default giftReducer;
ListContainer
import React, { Component } from 'react';
import { connect } from 'react-redux';
// import actions from action creators file
import * as actions from '../Actions/actions';
import AllGiftsDisplay from '../Components/AllGiftsDisplay.jsx';
import GiftCreator from '../Components/GiftCreator';
const mapStateToProps = (state) => ({
lastGiftId: state.gifts.lastGiftId,
giftList : state.gifts.giftList,
totalVotes: state.gifts.totalVotes,
setNewMessage: state.gifts.setNewMessage
});
//pass in text into update
const mapDispatchToProps = dispatch => ({
updateGiftMessage: (e) => {
console.log(e.target.value);
dispatch(actions.setMessage(e.target.value));
},
addGift: (e) => {
e.preventDefault();
console.log("actions: ", actions.addGift);
dispatch(actions.addGift());
}
});
class ListContainer extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div className="All-Lists">
<h1>LIST CONTAINER!</h1>
<AllGiftsDisplay giftList = {this.props.giftList} addGift={this.props.addGift} setNewMessage={this.props.setNewMessage} totalVotes = {this.props.totalVotes} lastGiftId = {this.props.lastGiftId}/>
<GiftCreator setNewMessage={this.props.setNewMessage} updateGiftMessage={this.props.updateGiftMessage} addGift={this.props.addGift}/>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ListContainer);
The secret is to avoid mutating giftList.
const giftReducer = (state=initialState, action) => {
// let giftList;
// let setMessage;
switch(action.type) {
case types.ADD_GIFT:
// create the new gift object structure.
const giftStructure = {
// lastGiftId: stateCopy.lastGiftId,
newMessage: stateCopy.newMessage,
totalVotes: 0
};
return {
...state,
giftList: [...state.giftList, giftStructure],
newMessage: ''
}
case types.SET_MESSAGE:
return {
...state, newMessage: action.payload,
}
case types.ADD_VOTE:
case types.DELETE_GIFT:
default:
return state;
}
};
To better understand why it's necessary not to mutate the array, consider this example:
const arr = [1, 2, 3];
const b = { a: arr };
const c = { ...b };
c.a.push(4);
console.log(arr === c.a); // outputs true
I'd like to fire interval after backend data will come in as a prop.
Take a look at this chunk of code:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getAdvert } from "../../actions/advert";
import './_advert.scss';
class Advert extends Component {
state = { counter: 0 };
componentDidMount() {
const { getAdvert } = this.props;
getAdvert();
}
componentWillReceiveProps(nextProps) {
const { counter } = this.state;
this.bannerInterval = setInterval(() => {
this.setState({ counter: counter === Object.keys(nextProps.banners).length - 1 ? 0 : counter + 1 });
}, 1000)
}
render() {
const { banners } = this.props;
const { counter } = this.state;
return (
<div className="advert__container">
<img src={banners[counter] && banners[counter].image_url} alt="advert" />
</div>
);
}
}
const mapStateToProps = ({ banners }) => {
return { banners };
};
const mapDispatchToProps = (dispatch) => {
return {
getAdvert: () => dispatch(getAdvert())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Advert);
So as you can see I tried to run it within componentWillReceiveProps method as I thought it might be proper place to be dependent on incoming props. But that won't work. I will run only once and interval will be repeating the same value.
Thanks for helping!
ComponentWillReceive props is extremely dangerous used in this way. You are istantiating a new interval every time a props is received without storing and canceling the previous one.
Also is not clear how do you increment counter, as I see the increment in your ternary will not increase the counter value incrementaly.
// This is an example, the lifeCylce callback can be componentWillReceive props
componentDidMount() {
const intervalId = setInterval(this.timer, 1000);
// store intervalId in the state so it can be accessed later:
this.setState({intervalId: intervalId});
}
componentWillUnmount() {
// use intervalId from the state to clear the interval
clearInterval(this.state.intervalId);
}
timer = () => {
// setState method is used to update the state with correct binding
this.setState({ currentCount: this.state.currentCount -1 });
}