How to fetch data by existing redux action with hooks? - reactjs

I'm trying to understand React Hooks. What I want is to fetch data inside functional component by call redux action with useEffect hooks.
I know that I can pass props to state like
const [todoList] = useState(props.todoList)
But what is the best practice to fetch data by existing redux actions?
In React class component i call this method to fetch data in componentDidMount() and everythink works.
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { ITodo } from './types'
import { getTodos } from '../actions/todoActions'
interface IProps {
todoList: Array<ITodo>
getTodos: typeof getTodos
}
const Todos = (props: IProps) => {
useEffect(() => {
props.getTodos()
}, [props.todoList])
return (
<div>
{props.todoList.map((_) => (<div key={_.Id}>{_.Name}</div>))}
</div>
)
}
const mapStateToProps = (state) => ({
todoList: state.todo.todoList
})
const mapDispatchToProps = {
getTodos
}
export default connect(mapStateToProps, mapDispatchToProps)(ProdRoute)
I expected to get list of todos with props and props.getTodos() should call once like in componentDidMount() method. But actualy I get data and getTodos() are called over and over again but should be called once on component mount

Take care that if you pass [props.todoList] to the useEffect you are erroneously forcing a constant refresh because:
useEffect does an instance comparison (===) to know if props.todoList is changed
after the the very first render the props.getTodos() dispatcher is called
when the props.todoList will be updated the component is re-rendered
the useEffect call will receive [props.todoList] as a value to check if it needs to re-run or not
props.todoList is changed (it was empty and now it's valorized) and props.getTodos() is so re-called
redux updates the todoList with the same values but mutating the array reference
the component is re-rendered and the useEffect will check if the [props.todoList] param is been updated... but IT IS BEEN UPDATED because the previous props.todoList is different from the actual props.todoList, even if the content is the same
So, if you need to call the props.getTodos() just once can
use [props.todoList.length] instead of [props.todoList] as the second parameter for the useEffect call
use an empty array [] as the second parameter for the useEffect call (see the docs)

Related

Why is the updated state variable not shown in the browser console?

I understand that the method returned by useState is asynchronous, however, when I run the this code, I am delaying the console.log by upto 5 seconds, but it still logs the previous value and not the updated value of the state variable. The updated value would be 2, but it still logs 1. In the react developer tools however, I can see the state changing as I press the button, though I am curious to know why after even such a delay the console prints an obsolete value? This is not the case with class components and setState but with function components and useState.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
setTimeout(() => {
console.log(variable);
}, 2000);
};
return <button onClick={handleClick}>Button</button>;
}
export default App;
In your code your setTimeout is getting the variable value from the closure at the time it was invoked and the callback function to the setTimeout was created. Check this GitHub issue for the detailed explanation.
In the same issue, they talk about utilizing useRef to do what you are attempting. This article by Dan Abramov packages this into a convenient useInterval hook.
State updates are asynchronous. That means, that in order to view the new value, you need to log It on the next render using useEffect and adding it to the dependencies array:
In this example, give a look at the order the logs appear:
First, you will have the current one, and once triggered, you will have the new value, and then it will become the 'old value' until triggered again.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(`new state rolled: ${counter}`);
}, [counter]);
console.log(`Before rolling new State value: ${counter}`);
const handleClick = () => setCounter(counter++)
return <button onClick={handleClick}>Button</button>;
}
export default App;
Another technic to console.log a value afterward a state change is to attach a callback to the setState:
setCounter(counter++, ()=> console.log(counter));
I hope it helps.
A state take some time to update. The proper way to log state when it updates, is to use the useEffect hook.
setTimeout attaches the timer and wait for that time, but it will keep the value of variable from the beginning of the timer, witch is 1
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
};
useEffect(() => {
console.log(variable);
}, [variable]);
return <button onClick={handleClick}>Button</button>;
}
export default App;
This is not the case with class components and setState but with
function components and useState
In class components, React keep the state in this.state & then call the Component.render() method whenever its need to update due to a setState or prop change.
Its something like this,
// pseudocode ( Somewhere in React code )
const app = MyClassComponent();
app.render();
// user invoke a callback which trigger a setState,
app.setState(10);
// Then React will replace & call render(),
this.state = 10;
app.render();
Even though you cannot do this.state = 'whatever new value', React does that internally with class components to save the latest state value. Then react can call the render() method and render method will receive the latest state value from this.state
So, if you use a setTimeout in a class component,
setTimeout(() => {
console.log(this.state) // this render the latest value because React replace the value of `this.state` with latest one
}, 2000)
However in functional component, the behaviour is little bit different, Every time when component need to re render, React will call the component again, And you can think the functional components are like the render() method of class components.
// pseudocode ( Somewhere in React code )
// initial render
const app = MyFuctionalComponent();
// state update trigger and need to update. React will call your component again to build the new element tree.
const app2 = MyFunctionalComponent();
The variable value in app is 1 & variable value in app2 is 2.
Note: variable is just a classic variable which returned by a function that hooked to the component ( The value save to the variable is the value return by the hook when the component was rendering so it is not like this.state i.e its hold the value which was there when the component is rendering but not the latest value )
Therefore according to the Clouser, at the time your setTimeout callback invoke ( Which was called from app ) it should log 1.
How you can log the latest value ?
you can use useEffect which getting invoke once a render phase of a component is finished. Since the render phase is completed ( that mean the local state variables holds the new state values ) & variable changed your console log will log the current value.
useEffect(() => {
console.log(variable);
}, [variable])
If you need the behaviour you have in class components, you can try useRef hook. useRef is an object which holds the latest value just like this.state but notice that updating the value of useRef doesn't trigger a state update.
const ref = useRef(0);
const handleClick = () => {
setVariable(2); // still need to setVariable to trigger state update
ref.current = 2 // track the latest state value in ref as well.
setTimeout(() => {
console.log(ref.current); // you will log the latest value
}, 2000);
};

When does useEffect call when I use it with useContext?

I call useEffect inside useContext and I want to know when this useEffect is called.
[settingContext.tsx]
// create context object
export const ColorContext = createContext<ColorContextType>(null);
export const ProductsProvider = (props) => {
const { query } = useRouter();
const [data, setData] = useState<ColorContextType>(null);
useEffect(() => {
async function fetchAPI() {
const res = await fetch(`${env.API_URL_FOR_CLIENT}/page_settings/top`);
const posts = await res.json();
setData(posts);
}
fetchAPI();
}, []);
return <ColorContext.Provider value={data}>{props.children}</ColorContext.Provider>;
};
export const useColorContext = () => {
const colors = useContext(ColorContext);
let themeColor: string = '';
let titleColor: string = '';
if (colors !== null) {
const colorData = colors.response.result_list[3].value;
themeColor = JSON.parse(colorData).theme_color;
titleColor = JSON.parse(colorData).title_color;
}
return { themeColor, titleColor };
};
[_app.tsx]
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<LayoutInitial>
<ProductsProvider>
<Component {...pageProps} />
</ProductsProvider>
</LayoutInitial>
);
}
I use useColorContext on multiple components.
It seems like useEffect is only called on '/' page, which is fine but I'm curious that useEffect should be called every time pages are rendered but it seems that it doesn't.
is this because I use useContext?
The useEffect call is done in the ProductsProvider component which appears to only be rendered once, on page load/refresh. This is because components generally only re-render when a state they subscribe to is changed. If the useEffect were called directly within the <Component> component, it would be called every time the component is mounted (not in re-renders). useEffect is only called multiple times after mounting if one of its dependencies changes, which in your case, there are none.
For example: this sandbox
It's composed of the App component, containing a Router to a homepage, a ComponentA route, and a ComponentB route. When each component mounts, its useEffect is called, creating an alert box. You'll only see the App useEffect alert once per page refresh.
ComponentA will have its useEffect called when the component mounts (every time you hit the /a route from a different route), and when the state in the component changes, since it's in the useEffect dependency array.
ComponentB will only have its useEffect called when the component mounts, and not when its state changes, because the state isn't included in the useEffect dependency array.
EDIT: To clarify, your useColorContext hook is not actually part of the ProductsProvider component, so the useEffect call is not "inherited" by any components that call the hook. Also, keep in mind when experimenting that using Strict Mode will cause components to render twice, allowing react to gather information on the first render, and display it on the second render.

React usEffect firing when it shouldn't be

I have a react component with a prop that is passed by a redux connect method. There is a useEffect linked specifically to that prop that is supposed to perform an async call when it changes. The problem is the useEffect fires any time I change the redux state anywhere else in that app, despite the prop I have the useEffect attached to not changing.
The useEffect method looks like this
useEffect(() => {
if (userPhoneNumber) {
myAsyncFunction()
.then(() => {
showData()
})
}
}, [userPhoneNumber])
And the userPhoneNumber prop is passed via the react-redux connect method like so:
const mapStateToProps = state => {
return {
userPhoneNumber: state.appState.userPhoneNumber
}
}
export default connect(mapStateToProps)(MyComponent)
From what I understand this could be breaking in two potential places. for one, useEffect should not be firing if the userPhoneNumber prop doesn't change. Also, the mapStateToProps method is not returning a new value so it should not be triggering any sort of a rerender.
The redux state changes that are leading to the unexpected useEffect call are coming from sibling components that have a similar react-redux connect setup to this component, with different state to prop mappings.
When a reducer runs you'll have an entirely new state object, thus a new userPhoneNumber prop reference. You should memoize the userPhoneNumber value. I use reselect to create memoized state selectors, but you could probably use the useMemo hook and use that memoized value in the dependency for the effect.
const memoizedUserPhoneNumber = useMemo(() => userPhoneNumber, [userPhoneNumber]);
useEffect(() => {
if (memoizedUserPhoneNumber) {
myAsyncFunction()
.then(() => {
showData()
})
}
}, [memoizedUserPhoneNumber]);
Using Reselect
import { createSelector } from 'reselect';
const appState = state => state.appState || {};
const userPhoneNumber = createSelector(
appState,
appState => appState.userPhoneNumber;
);
...
const mapStateToProps = state => {
return {
userPhoneNumber: userPhoneNumber(state),
}
}
None of this helps keep the entire functional component from re-rendering when the parent re-renders though, so to help with this you'll need to help "memoize" the props being fed into the component with the memo HOC.
export default memo(connect(mapStateToProps)(MyComponent))

Avoid re render with react hooks

I have some value that comes from Redux through props and I want to NOT render the component again when this value changes.
I found some answers saying that I can use Memo but I don't know if this is the best option for my case?
My "code":
const MyComponent = () => {
return ...;
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default connect(mapStateToProps)(MyComponent);
myVar changing shouldn't re render the component in this case.
React.memo can do the job, you can pass a custom equality check function to perform a rerender only when it returns a falsy value. I never faced a case where you want to completely ignore a value update from your Redux store, maybe it shouldn't be stored there ?
Memo API
eg: React.memo(Component, [areEqual(prevProps, nextProps)])
UseSelector API
Another way would be to use useSelector with a custom equality check function:
useSelector Redux API Reference
Connect API
If you still want to stick with mapStateToProps, you can also pass a custom equality check function as a parameter of the connect function:
areStatePropsEqual Redux API Reference
Edit: useRef solution
By using useRef, you store a mutable variable that will be kept as it is for the whole lifetime of the component.
Example based on yours:
const StoreMyVar = (WrappedComponent) => ({myVar, ...props}) => {
const myRefVar = useRef(myVar)
return <WrappedComponent myVar={myRefVar} {...props} />
}
const MyComponentWithImmutableVar = StoreMyVar(MyComponent)
The fastest way is React.memo, but you can use it just with functional components. Be careful, it is not tested.
const MyComponent(props) {
return ...;
}
const areEqual(prevProps, nextProps) {
return prevProps.myVar === nextProps.myvar
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default React.memo(MyComponent, areEqual);

PureComponent keeps rendering even though no state/prop changes

I'm trying to learn and test React.PureComponent and it keeps rendering even though no state changes for that pure component.
My PureComponent is very simple and it accepts only one Redux Action function via connect hoc
import React from 'react';
import {
Container,
Button
} from 'reactstrap'
import { connect } from 'react-redux'
import { resetWorkouts } from '../actions/workoutApiActions'
class About extends React.PureComponent {
render () {
const { resetWorkouts } = this.props;
console.log('in about render...')
return (
<React.Fragment>
<Container>
<h2>Api Data Reset</h2>
<Button color="danger" onClick={resetWorkouts}>Reset Data</Button>
</Container>
</React.Fragment>
);
}
}
const mapDispatchToProps = dispatch => {
return {
resetWorkouts: () => dispatch(resetWorkouts())
}
}
export default connect(null, mapDispatchToProps)(About);
In the above code, you can see that, there is no state in the component. It only accepts the action function as props from connect. However, whenever I clicks on the Reset Data button, it keeps calling the render method as shown in the screenshot.
In the screenshot, I can see that, global state store has been changed whenever, I click a button. But, that state is not used in my PureComponent and it should be out of the scope and my component should ignore to re-render.
Or Redux Action functions are created every time the global state store has been changed. And passed as a new object to my PureComponent ?
Theoretically, I don't need to write my own shouldComponentUpdate function, right? I'm confused and could you please help me to understand about this behaviour?
My goal is I don't want my PureComponent to render again when a user clicks a button.
Updates:
I have tried like the following according to this article and it's still re-rendering
const mapDispatchToProps = {
resetWorkouts
};
this because react do a shallow comparison between the prevProps and the nextProps,
and you can control that only in the shouldComponentUpdate, react doesn't know that the dispatcher is the same one from the previous render, because you are using return inside the mapDispatchToProps function.
In your component and in your case, while the function will remain the same, you can go with two paths:
path 1:
override the shouldComponentUpdate life cycle hook, to be as the following:
shouldComponentUpdate(){
return false;
}
path 2:
get rid of the return inside mapDispatchToProps and simplify the connect so it be as the following:
`conncect(state => ({}), {
resetWorkouts: resetWorkouts})(YourComponent);`
using one of the above paths should make you good to go
The reason why your component is rendering is because everytime the following function executes:
const mapDispatchToProps = dispatch => {
return {
resetWorkouts: () => dispatch(resetWorkouts())
}
}
your components receives a new instance of a property named resetWorkouts(because you're creating an inline array function). You may look at the ownProps to check if your component already have the resetWorkouts:
const mapDispatchToProps = (dispatch, ownProps) => {
return {
resetWorkouts: ownProps.resetWorkouts || () => dispatch(resetWorkouts())
}
}

Resources