What causes re-rendering? and how to avoid it - reactjs

I'm having some troubles in a implementation with React, Redux and Hooks.
I do not know how to avoid this:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import * as R from "ramda";
function Main() {
const mainBanners = useSelector(state => state.mainBanners);
const features = useSelector(state => state.features);
const banners = useSelector(state => state.banners);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchMainBanners());
dispatch(fetchFeatures());
dispatch(fetchBanner());
}, [dispatch]);
console.log(features);
return (
<div className="Main">
{R.isEmpty(mainBanners || features) ? (
<p>Loading...</p>
) : (
<MainBanner mainBanners={mainBanners} features={features} />
)}
<Banners banners={banners} />
</div>
);
}
export default Main;
The result console.log of this example is as follows:

You have three different useSelector calls, and you are executing fetches for three different sets of data. So yes, I would expect that to result in a total of four separate renders:
Initial render
Render after mainBanners is fetched
Render after features is fetched
Render after banners is fetched
This is both expected based on the code you have written, and fine in general, given that your component might want to render something different when any of those changes.

Related

Can you use setReduxObject and selectReduxObject in the same .jsx page?

I am learning Redux and I have deviated from the instructor's code. I am trying to convert my code from context & state into Redux.
Is it advisable to use setReduxObject (setCategoriesMap in my code) and selectReduxObject (selectCategoriesMap in my code) in the same .jsx page? Are there any concerns around this?
Thanks!
My code:
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCategoriesAndDocuments } from "../../utils/firebase/firebase.utils";
import { setCategoriesMap } from "../../store/categories/categories.action";
import { selectCategoriesMap } from "../../store/categories/categories.selector";
import Category from "../../components/category/category.component";
const Shop = () => {
const dispatch = useDispatch();
useEffect(() => {
const getCategoriesMap = async () => {
const categories = await getCategoriesAndDocuments();
dispatch(setCategoriesMap(categories));
};
getCategoriesMap();
}, []);
const categoriesMap = useSelector(selectCategoriesMap);
return (
<div>
{Object.keys(categoriesMap).map((key) => {
const products = categoriesMap[key];
return <Category key={key} title={key} products={products} />;
})}
</div>
);
};
export default Shop;
This is just the default approach, nothing to be concerned about.
As soon as you're using getCategoriesAndDocuments the same way in another component though, it's better to move this to an async action creator.
Could even do it for this component already to improve separation of concerns. The component does not necessarily need to be involved with firebase, its job is display logic. Wether the data comes from firebase or localStorage or some graphQL server should not matter.

Cannot update a component (`TodoForm`) while rendering a different component (`TodoTask`). [SOLUTION] [React Redux To-Do App]

WHILE WRITING THIS POST I REALIZED WHAT THE SOLUTION WAS
Every time I dispatch a task to my store the following error occurs:
I have some idea of why it happens. It happens precisely when I try to get the to-do list using useSelector and then mapping through the list. However, the mapping is not the issue but rather returning a react component on the map function. It works just fine if I do not return a functional component and instead use HTML. So the issue, from my POV, is returning a react functional component while passing props to it on a map function.
Here's the code for my home component:
import Input from '../components/Input';
import TodoForm from '../components/TodoForm';
function Home() {
document.title = "MyTodo | Home"
return (
<div className="App">
<h1>MyTodo</h1>
<Input />
<TodoForm />
</div>
);
}
export default Home;
The input component where the action is being dispatched on key down:
import {useState} from 'react'
import { useDispatch } from 'react-redux';
import { todoActions } from '../store/todo';
const Input = () => {
const [inputText, setInputText] = useState("");
const dispatch = useDispatch();
const handleChange = (e) => setInputText(e.target.value)
const handleKeyPress = (event) => {
if (event.code === "Enter") {
// if the expression is false, that means the string has a length of 0 after stripping white spaces
const onlyWhiteSpaces = !inputText.replace(/\s/g, "").length;
!onlyWhiteSpaces &&
dispatch(
todoActions.addTask({ label: inputText, done: false })
);
setInputText("");
}
};
return (
<input
type="text"
onKeyDown={(e) => handleKeyPress(e)}
onChange={(e) => handleChange(e)}
value={inputText}
/>
);
}
export default Input
The TodoForm where I am using useSelector to get the todo list from the redux store and mapping thru it:
import { useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import TodoTask from "./TodoTask";
const TodoForm = () => {
const tasks = useSelector((state) => state.todo.taskList);
const renderedListItems = tasks.map((task, index) => {
return (
<TodoTask
key={uuidv4()}
task={task}
targetIndex={index}
/>
);
});
return <div className="container">{renderedListItems}</div>;
};
export default TodoForm;
Finally the TodoTask component which is the child component being returned on the map function above:
import { useDispatch } from "react-redux";
import { todoActions } from "../store/todo";
const TodoTask = ({ task, targetIndex }) => {
const {text, done} = task;
console.log("Task: ", task);
const dispatch = useDispatch()
const removeTask = dispatch(todoActions.deleteTask(targetIndex))
return (
<div
className="alert alert-primary d-flex justify-content-between"
role="alert"
>
{text}
<button type="button" className="btn-close" onClick={()=>removeTask}></button>
</div>
);
};
export default TodoTask;
This is my first time facing this issue, and I know it has something to do with redux and how the useSelector hook forces a component to re-render. So the useSelector is re-rendering the TodoForm component, and since we are mapping and returning another component, that component is also being rendered simultaneously. At least, that is how I understand it. Let me know if I am wrong.
Things I have tried:
Wrapping the TodoTask in React.memo. Saw it somewhere as a possible solution to this kind of issue, but that did not work.
Passing shallowEqual as a second parameter on the TodoForm useSelector. This does prevent the page from going into an infinity loop, but the tasks show up empty but are being added to the redux store. However, with this method, the first warning stills shows up, and the console log in the TodoTask component does not execute.
Passing shallowEqual as a second parameter on the TodoForm useSelector. This does prevent the page from going into an infinity loop but the tasks show up empty but are being added to the redux store. However, with this method, the first warning stills shows up and the console log in the TodoTask component does not execute.
I realized what I was doing wrong while writing this part. The console log in the TodoTask component was working, but I had the browser console filtering for errors only. When I check the messages section, I saw everything working fine. Then when I checked the Task component, I noticed I was trying to read a property that did not exist and hence why the tasks had no text.
In other words, the solution was adding shallowEqual as second parameter of the useSelector hook in my TodoForm component that was the one mapping thru the todo tasks array. As I said, useSelector forces a component to re-render. shallowEquals checks if the existing state isn't the same as we already had and avoids unnecessary re-renders, which can lead my application to exceed the maximum update length.
Code fix [Solution]:
import { memo } from "react";
import { shallowEqual, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import TodoTask from "./TodoTask";
const TodoForm = () => {
// shallowEqual prevents unnecessary re-renders which can lead to an infinite loop
// it compares the current state with the previous one, if they are the same, it does not re-render the component
const tasks = useSelector((state) => state.todo.taskList, shallowEqual);
const renderedListItems = tasks.map((task, index) => {
return (
<TodoTask
key={uuidv4()}
task={task}
targetIndex={index}
/>
);
});
return <div className="container">{renderedListItems}</div>;
};
export default memo(TodoForm);
Honestly, I have been stuck on this since yesterday and I cannot believe I realize the solution just when I was about to ask for help. Hope this helps anyone else who faces a similar issue in the future.

React JS coponent not rendering using map function

I hava a component called videoRow i try to render this component using dummy values now i get data from a useEffect Hook i have to use that data to render my component but when i try to do so it dont show anything. I even try console log to check weather i get my data or not it print my data on console means my useEffect is working But when i try this data on my videoRow component it not show anything
import React, { useState, useEffect } from "react";
import "../css/searchPage.css";
import TuneSharpIcon from "#mui/icons-material/TuneSharp";
import ChannelRow from "./ChannelRow";
import VideoRow from "./VideoRow";
import { selectInput } from "../features/inputSlice";
import { useSelector } from "react-redux";
import Axios from "axios";
function SearchPage() {
const getQuery = useSelector(selectInput);
const API_URL = `https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=4&key=APIKEY&type=video&q=${getQuery.input}`;
const [data, setData] = useState([]);
useEffect(() => {
async function fetchData() {
let request = await Axios.get(API_URL);
setData(request);
}
fetchData();
}, [API_URL]);
console.log(data);
return (
<div className="searchPage">
<div className="filter">
<TuneSharpIcon></TuneSharpIcon>
<h2>FILTERS</h2>
</div>
<hr></hr>
<ChannelRow
image="https://images.indianexpress.com/2022/01/Republic-Day_1200_AP2022.jpg"
channelName="Dummy"
verified
subs="670k"
noOfVideos={567}
desc="You can find awesome programming lessons here! Also, expect programming tips and tricks that will take your coding skills to the ..."
></ChannelRow>
<hr></hr>
{data?.data?.items?.forEach((item) => {
console.log(item.snippet.title);
console.log(item?.snippet.thumbnails.high.url)
console.log(item?.snippet.publishedAt)
console.log(item?.snippet.description)
console.log(item?.snippet.channelTitle)
return(<VideoRow
image={item?.snippet.thumbnails.high.url}
channelName={item?.channelTitle}
timestamp={item?.snippet.publishedAt}
title={item?.snippet.title}
desc={item?.snippet.description}
views="1.4M"
subs="1.4M"
></VideoRow>)
})}
</div>
);
}
export default SearchPage;
Change data?.data?.items?.forEach to data?.data?.items?.map. forEach returns nothing. So, even if you return the component from the callback, forEach will just ignore it. But, map will return all transformed results as an array.
You can read more about lists in react here.

useEffect socket.on problem rerendering useState

I'm looking for some help using socket.io-client. I'm having some issues trying to get all messages from the chat server. I'm supposed to get an individual message and I'll need to push to a state. I'm not getting rerender on each message. Is there any way to handle the state and update it? Thanks
import React, {useState, useEffect} from 'react'
import {withRouter} from 'react-router-dom'
import Button from '../core/Button'
import Field from '../core/Field'
import Label from '../core/Label'
import {Container, ScrollContainer} from './StyledComponents'
import useLocalStorageState from '../../hooks/useLocalStorage'
import io from 'socket.io-client'
import {SOCKET_SERVER_CLIENT} from '../../utils/const'
import Profile from '../Profile/Profile'
let socket
const Chat = ({location}) => {
const [listMessages, setListMessages] = useState([])
const [message, setMessage] = useState('')
useEffect(() => {
const username = location.search.split('=')[1]
socket = io(`${SOCKET_SERVER_CLIENT}/?username=${username}`)
return () => {
socket.disconnect()
}
}, [])
useEffect(() => {
socket.on('message', message => {
const auxArr = listMessages
auxArr.push(message)
setListMessages(auxArr)
})
})
return (
<Container>
{console.log('rerender')}
<ScrollContainer>
{listMessages &&
listMessages.map(infoMessage => (
<Profile key={infoMessage.time} {...infoMessage} />
))}
</ScrollContainer>
</Container>
)
}
export default withRouter(Chat)
const auxArr = listMessages
auxArr.push(message)
setListMessages(auxArr)
This code is mutating the old array, and then setting state with that same array. Function components do a check when setting state to make sure it changed. Since it's the same array before and after, the render gets skipped.
Instead of mutating the state, you will need to copy it:
const auxArr = [...listMessages];
auxArr.push(message);
setListMessages(auxArr);
Since you're basing the new state on the old one, you should also use the callback form of setting state, to eliminate any bugs that might happen if multiple set states happen at right about the same time:
setListMessage(prevMessages => {
const auxArr = [...prevMessages];
auxArr.push(message);
return auxArr;
})

Can't map array with useEffect and React Redux

I am trying to map over an employee list after useEffect fetches the array and I dispatch it to my redux-state. If I console log it I see an array full of objects, but my component doesn't appear to be re-rendering. Any suggestions?
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setEmployees } from '../redux/reducers/employeeListReducer'
import employeesService from '../back-end/services/employees'
const EmployeeList = ({ }) => {
const dispatch = useDispatch()
const employees = useSelector(state => state.employeeList)
const user = useSelector(state => state.user)
employeesService.setToken(user.token)
useEffect(() => {
(async () => {
const employeeList = await employeesService.getAll()
dispatch(setEmployees(employeeList))
})();
}, [dispatch])
if (employees === null) {
return <p>Loading Employees...</p>
}
return (
<div>
{console.log(employees)}
{employees.map(e =>
<div key={e.id}>
{e.name} {e.phone} {e.email}
</div>
)}
</div>
)
}
export default EmployeeList
It is unclear from the description what may be causing the first issue you describe (there is no console.log we can look at to determine why there is a discrepancy), but you cannot call hooks from within callbacks provided to other hooks. This is a limitation of the how hooks are implemented. They depend on always being called in the same order (so you cannot conditionally call a hook function), and they must all have been called by the time your function returns. Details can be found here:
https://reactjs.org/warnings/invalid-hook-call-warning.html
Basically, you must call hook functions within the top level of a functional component, or within the top level of another hook (for instance, you will see hooks that are implemented using other hooks). All hook functions must have been called by the time your function returns. If the hook is called within a callback, it very likely will be called after your function returns. So React warns you about this.
All of that said, moving your declaration of dispatch to the top level of your component should resolve this issue:
import React, {useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setEmployees} from '../redux/reducers/employeeListReducer'
import employeesService from '../back-end/services/employees'
const EmployeeList = ({ employee }) => {
const dispatch = useDispatch()
const employees = useSelector(state => state.employeeList)
const user = useSelector(state => state.user)
employeesService.setToken(user.token)
useEffect(() => {
employeesService
.getAll()
.then(employeeList => {
dispatch(setEmployees(employeeList))
})
}, [dispatch])
return (
<div>
Employee List Placeholder
{employees.map(e =>
<div key={e.id}>
{e.name} {e.phone} {e.email}
</div>
)}
</div>
)
}
export default EmployeeList

Resources