Any change to redux store my causes component to re-render - javascript

I'm doing some testing on my UI and I've noticed that if any state changes in my redux store my component (shown below) re-renders and restarts with embedded video at 0. If I type in a redux-connected text field, it remounts, if a status notification hits the store, it remounts, etc.
I have no idea how to fix this and I could really use some help figuring out how to go after the bug.
tldr; How can I stop my VideoPlayer from re-rendering each time something changes in my redux store?
redux-toolkit
react
component
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm);
// renders output here in the same component
return (
...
{sourceContentFound === false ? (
<VideoPlayerDisabled />
) : (
<VideoPlayerController
title={title}
description={description}
sourceAddress={iframeURL}
author={authorName}
timeStamp={{ startTime: 0 }}
/>
)}
)
...
}
formSlice
export const dynamicFormSlice = createSlice({
name: 'dynamicForm',
initialState,
reducers: {
updateField: (state, action) => {
state = action.payload;
return state;
}
},
});
export const selectDynamicForm = createSelector(
(state: RootState): dynamicFormState => state.dynamicForm,
dynamicForm => dynamicForm
);
statusHandlerSlice
I don't think this component does anything crazy, per-say, but I have a notification appear when the video conditions are met. When it goes back down clearStatus the video player restarts.
export const statusHandlerSlice = createSlice({
name: 'statusHandler',
initialState,
reducers: {
setStatus: (state, action: PayloadAction<IStatusObject>) => {
const { type, display, message } = action.payload;
state.status = {
...action.payload,
message: message.charAt(0).toUpperCase() + message.slice(1),
};
if (display === 'internal-only' || display === 'support-both') {
statusLogger(type, message);
}
},
clearStatus: state => {
state.status = {
type: 'success',
data: {},
message: '',
display: 'internal-only',
key: '',
};
},
},
});
export const { setStatus, clearStatus } = statusHandlerSlice.actions;
export const selectStatus = (state: RootState): IStatusObject =>
state.statusHandler.status;

Your MyComponent is re-render every time redux store state change is because you have a selector in it
You could stop this to happen by, add an equalityFn to useSelector.
You can write your own equalityFn or use some existing function from a library that supports deep comparison.
Ex: Use lodash isEqual
import { isEqual } from 'lodash';
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm, isEqual);
By default, useSelector use a shallow compare which can't detect deep changes inside your object, change to a deep comparison function like isEqual will help you to do that, but It's not recommended for all selector since there will be a performance impact.
Live Example:

I suggest either creating a custom equalFn to compare the data you're using in the current component or do not select the whole slice, maybe some properties change is unnecessary for your component. like:
const { data } = useSelector(store => store.sliceA, shallowEq);
// console.log(data) => { a: "foo", b: "bar" }
// data.b is useless but once it is changed, the component will re-render as well
return <Typography>{data.a}</Typography>
You should install React Devtools, turn on profiler, remember to check Record why each component rendered while profiling in setting to see what is causing re-rendering. sometimes custom hooks in libraries trigger re-rendering.
whyDidYouRender
is a good choice too

Related

How to indicate to TS that value in store has value even though it's optional?

I have the following case:
I have a standard store with optional items in it.
I also have a tree of elements which rely on that store. I also select {account} in multiple components.
For business logic, I had to check at the very top if account is set. If it is not, I don't render the components which rely on it.
How can I tell TS that even though the value is optional in store I'm 100% sure it is NOT undefined?
Example code:
// store
interface Account {
id: number;
name: string;
}
export interface AppState {
account?: Account;
}
const initialState: AppState = {};
const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount(state: AppState, action: PayloadAction<Account | undefined>) {
state.account = action.payload;
}
}
});
// component
const GrandChild = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild /> : <>account not set</>;
};
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
// dispatch(setAccount({ id: 0, name: "john" }));
});
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Child />
</div>
);
}
Codesandbox:
https://codesandbox.io/s/required-reducer-ysts49?file=/src/App.tsx
I know I can do this:
const { account } = useSelector((state: RootState) => state, shallowEqual) as Account;
but this seems very hacky. Is there a better way?
Well you know that Grandchild wont be rendered if account is undefined. so why not pass account as a property to the Grandchild component. This way you could define the property as never being undefined. And since you only render Grandchild after you checked that account isn't undefined you should be able to pass account as the property to the component (and since you defined the property as not being undefined TS will not object to account.name in your Grandchild component.
I don't know redux however - I have never used it and don't know anything about it, so I don't know if this answer is compatible with that or if redux will cause some issues I couldn't forsee.
I've written a little bit of code of how this could look (but as I already said, I don't know how to use redux, so you'll probably have to take my idea and write it so everything works) - so my code example is probably more of a visualization of what I mean than a solution.
const GrandChild = (account: Account) => {
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild account={account} /> : <>account not set</>;
};

React hook, wired issue when use useState, while if use setState work perfectly, how to solve it

dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.

Using redux to take a time on a stopwatch and put it into a database

I was recommended using redux to place a time into a database but I'm having issues. I have a stopwatch in my index.js that the user can start and stop. After, I have a button that allows them the ability to to put their time into a database. From my node file, I'm getting UnhandledPromiseRejectionWarning: error: null value in column "time" violates not-null constraint. I'm wondering if I'm getting that because I have difference = 0 at the stop of the index file and it doesn't retrieve difference = this.state.endTime - this.state.startTime; or if there is another issue I'm not seeing.
index.js
export let difference = 0;
export default class Home extends React.Component {
constructor(props);
super(props);
this.state = {
startTime: 0,
endTime: 0,
}
}
handleStart = () => {
this.setState({
startTime: Date.now()
})
}
handleStop = () => {
this.setState({
endTime: Date.now()
})
}
render() {
difference = this.state.endTime - this.state.startTime;
return (
<App />
<reducer />
);
}
}
reducer.js
import * as actionTypes from "./actions.js";
import difference from "../pages/index.js";
const initialState = {
count: 0
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.NUMBER:
return { state, count: difference };
default: return state;
}
};
export default reducer;
store.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
actions.js
export const NUMBER = 'NUMBER';
App.js
import React from 'react';
import { addName } from "./util";
function App() {
const [name, setName] = React.useState("")
function handleUpdate(evt) {
setName(evt.target.value);
}
async function handleAddName(evt) {
await addName(name);
}
return <div>
<p><input type='text' value={name} onChange={handleUpdate} /></p>
<button className='button-style' onClick={handleAddName}>Add Name</button>
</div>
}
export default App;
util.js
import "isomorphic-fetch"
import difference from "../pages/index.js";
export function addName(name) {
return fetch('http://localhost:3001/addtime', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, time: difference })
})
}
node server.js
app.post("/addtime", cors(), async (req,res) => {
const name = req.body.name;
const time = req.body.time;
const timeStamp = dateFormat(time, dateFormat.masks.isoDateTime);
const template = 'INSERT INTO times (name,time) VALUES ($1,$2)';
const response = await pool.query(template, [name,time]);
res.json({name: name, time: time});
});
State Management
In React we manage changing values through state. There is a way to access the value and a way to update the value. When the value changes, React knows to re-render the component.
This state can be stored within a component of your app. When you write const [name, setName] = React.useState(""), name is your "getter" and setName is your "setter".
The state can also be stored inside a Context Provider component that is a parent of the current component. Any children of this Provider can use the useContext hook to access the stored value.
The React Redux package uses these React contexts to make the redux state globally available. You wrap your entire App in a Redux Provider component to enable access.
With Redux, your "getters" are selector functions, which you call though useSelector, and your "setters" are actions that you pass though the dispatch function. React actually supports this syntax of state management on a local component level (without Redux) though the useReducer hook.
You don't need to use contexts in order to pass around state from component to component. You can also pass values and "setters" as props from a parent component to a child. React recommends this approach and has a section on Lifting State Up.
This is just a brief overview of some of the concepts at play. The issues that you are having here are due to state management. There are many possible ways to handle your state here. In my opinion Redux and Contexts are both unnecessary overkill. Of course you can use them if you want to, but you need to set them up properly which you haven't done.
Errors
export let difference = 0;
Variables which exist at the top-level of a file, outside of a component, should be immutable constants only. When you have a value that changes, it needs to be part of your app state.
When you have a "stateful" value like difference you can't just use a variable that exists outside of your components.
render() {
difference = this.state.endTime - this.state.startTime;
...
This is not how you update a value. We don't want to be just setting a constant. We also don't want to trigger changes of a stateful value on every render as this creates infinite re-render situations.
handleStart & handleStop
The functions themselves are fine but they are never called anywhere. Therefore difference will always be 0.
<reducer />
A reducer is not a component so you cannot call it like this. It is a pure function. In order to introduce redux into your JSX code, you use the reducer to create a store variable and you pass that store to a react-redux Provider component.
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.NUMBER:
return { state, count: difference };
default: return state;
}
};
You want to include the difference as a property of your action rather than accessing an external variable. The standard convention is to add a payload property to the action. The payload could be the difference number itself or an object with a property difference. That sort of design choice is up to you. An action might look like { type: actionTypes.NUMBER, payload: 0.065 } or { type: actionTypes.NUMBER, payload: { difference: 0.065 } }.
Your state is an object with a property count. Your reducer should return the next state, which should also be an object with a property count. This will not return the right state shape: return { state, count: difference };.
It is typical to use the spread operator ... to copy all other properties of the state and update just one (or a few), like this: return { ...state, count: difference }. However your state does not have any other properties, so that is the same as return { count: difference };. Since you are just storing a single number, there is no value from the previous state that is copied or preserved. (Which is a large part of why I think that Redux is not helpful or necessary here.)
There may be some issues on the backend as well, but there are such serious issues with the front end that I think that's your main problem.
Structuring Components
Think about what your app needs to do. What actions does it respond to? What information does it need to know?
Based on your description, it needs to:
Start a timer when a button is clicked
Stop a timer when a button is clicked
Store the most recent timer value
Allow a user to enter and update a name
Store that name
POST to the backend when a button is clicked, if there is a stored difference and name
Next you break that up into components. Store each stateful value at the highest component in the tree which needs to access it, and pass down values and callbacks as props. The startTime and endTime are only used by the timer itself, but the timer need to set the difference after stopping. The difference would be stored higher up because it is also needed for the POST request.
Think about what buttons and actions are available at any given time, and what conditions are used to determine this. For example, you shouldn't be able to stop a timer before you have started it. So we would see if startTime is greater than 0.
Minimal Front-End Example
import * as React from "react";
const Timer = ({ setDifference }) => {
const [startTime, setStartTime] = React.useState(0);
const handleStart = () => {
setStartTime(Date.now());
};
const handleStop = () => {
const difference = Date.now() - startTime;
setDifference(difference);
};
return (
<div>
<button onClick={handleStart}>
Start
</button>
<button disabled={startTime === 0} onClick={handleStop}>
Stop
</button>
</div>
);
};
const Submission = ({ time }) => {
const [name, setName] = React.useState("");
function handleUpdate(evt) {
setName(evt.target.value);
}
async function handleAddName(evt) {
return await fetch("http://localhost:3001/addtime", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, time })
});
}
return (
<div>
<div>Time: {time}</div>
<input type="text" value={name} onChange={handleUpdate} />
<button onClick={handleAddName} disabled={name.length === 0}>
Add Name
</button>
</div>
);
};
const App = () => {
const [time, setTime] = React.useState(0);
return (
<div>
<Timer setDifference={setTime} />
{time > 0 && <Submission time={time} />}
</div>
);
};
export default App;
CodeSandbox Link
You might not be saving any value to DOB column, you need to allow NULL value. You should change to
DOB = models.DateField(auto_now = False, null = True)
You should also make sure DOB column in your table allows null.

How to update redux store for many components at the same time on click of a single button?

I have four stateful react components in a single page, they get updated simultaneously on the click of a single button.
I now want to use redux store to save the state of all the components.
I wrote react-redux connect for the first component and it works fine.
Then followed the same logic for the second component, but the store is not getting updated along with the first one.
How can I save the states of all the components at same time to store?
I think you can use this kind of structure.
The main page in which you are using the four stateful components should look like this.
class App extends React.Component {
render() {
const { commonState, updateCommonStateHandler } = this.props;
return (
<div>
<Component1 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component2 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component3 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component4 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
</div>
);
}
}
const mapStateToProps = state => {
return {
commonState: state.commonState
};
};
const mapDispatchToProps = dispatch => {
return {
updateCommonStateHandler: change => {
dispatch(() => ({
type: 'UPDATE_COMMON_STATE',
change
}));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
With the reducer
const updateCommonStateReducer = (state = {}, action) => {
const newState = extend({}, state);
if(action.type === 'UPDATE_COMMON_STATE') {
newState.commonState = newState.commonState || {};
extend(newState.commonState, action.change || {});
}
return newState;
};
So if you want to update the state from any child component, you should call the updateCommonStateHandler, which will dispatch an action to update the state.
On state update react will re-render all the components with new commonState.
PS: This is just a sample code explaining the situation, not the solution and it's
written in ES6

Redux state not updating with javascript object

I have this container and component and the object yeast that Im trying to put in my store. However, its not working whenever I try and save it. The object looks like this
{ yeastType : value, temp: value}
Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeast) => {
dispatch(actions.updateYeast(yeast))
}
}
};
const RecipeYeastContainer = connect(
mapStateToProps,
mapDispatchToProps
)(RecipeYeast);
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast)
};
I have no errors in the console or anything. When I open my redux dev tools its telling me the state has already been updated by the time the action is called. And thus only ever saving the first letter thats input into my field. It never persists it. Its really weird. Its also never showing up in the UI. I can type as much as I want and see the action firing and the state keeping the first letter but its not showing up in the input.
Whats weird is that when I change the code to pass in both yeastType and temp to the property function and construct the object in there it works. (See below)
This works: Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeastType, temp) => {
const yeast = {yeastType, temp}
dispatch(actions.updateYeast(yeast))
}
}
};
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast.yeastType, updatedYeast.temp)
};
I cannot figure out why this is happening. I thought I could just pass the object all the way through and not have to reconstruct it.
Do you dispatch your action correctly? And in using redux, you are not updating the state of the component, you're updating the store and then the value in component is from your mapStateToProps function that get from the store. Say it you're updating your store with the object named yourReducer store. ex:
Container.js:
const mapStateToProps = (state) => ({
inputValue: state.yourReducer.value
})
const mapDispatchToProps = (dispatch) => ({
inputHandler: (e) => {
dispatch(yourAction({type: 'CHANGE_INPUT', value: e.target.value}))
// The store catches the action and pass to the reducer that can read the action's type
// in `yourReducer` will catch the 'CHANGE_INPUT' type and
// return a new value as same as the value that passed from the action
}
})
export default connect(mapStateToProps)(Component)
Component.js
export default class Component extends React.Component {
{ your custom function here ... }
render() {
const { inputValue, inputHandler } = this.props
return (
<div>
{/* The input will be the same as the inputValue changed */}
<input value={inputValue} onChange={inputHandler} />
</div>
)
}
}
For debugging redux you can try this redux-devtool.

Categories

Resources