I'm trying to reselect middleware today and prevent unnecessary re-rendering.
Here is my reducer.js:
const INITIAL_STATE = {
dogs: 100,
cats: 12312302384
};
const pets = (state = INITIAL_STATE, action) => {
switch (action.type) {
case "CHANGE_DOGS":
return {
...state, dogs: state.dogs + 1
};
case "CHANGE_CATS":
return {
...state, cats: state.cats + 1
};
default:
return { ...state };
}
};
export default pets;
Here is my main.js:
import React from "react";
import { createSelector } from "reselect";
import { useSelector, useDispatch } from "react-redux";
function ReduxDeneme(props) {
// Selectors - Classic style - NON Memoized!
// const dogsData = useSelector(state => state.pets.dogs);
// const catsData = useSelector(state => state.pets.cats);
// Dogs rendering.. --> First opening..
// 10:11:28.070 index.js:18 CATS rendering.. --> First opening..
// 10:11:29.703 index.js:13 Dogs rendering.. --> Press "ChangeDogs" button.
// 10:11:29.703 index.js:18 CATS rendering.. --> Press "ChangeDogs" button.
// 10:11:33.143 index.js:13 Dogs rendering.. --> Press "ChangeCats" button.
// 10:11:33.143 index.js:18 CATS rendering.. --> Press "ChangeCats" button.
// Selectors - Memoized version RESELECT middleware'i ile..
const dogsDataMemo = createSelector(
state => state.pets.dogs,
dogs => dogs
);
const catsDataMemo = createSelector(
state => state.pets.cats,
cats => cats
);
const dogsData = useSelector(dogsDataMemo)
const catsData = useSelector(catsDataMemo)
// Components
const Dogs = ({ dogsData }) => {
console.log("Dogs rendering..");
return <h1>{dogsData}</h1>;
};
const Cats = ({ catsData }) => {
console.log("Cats rendering..");
return <h1>{catsData}</h1>;
};
// Actions
const dispatch = useDispatch();
const changeDogs = () => dispatch({ type: "CHANGE_DOGS" });
const changeCats = () => dispatch({ type: "CHANGE_CATS" });
return (
<div>
<Dogs dogsData={dogsData} />
<Cats catsData={catsData} />
<button onClick={changeDogs}>Change Dogs</button>
<button onClick={changeCats}>Change CATS</button>
</div>
);
}
export default ReduxDeneme;
1. Scenario: I have used classic, non-memoized style selectors. There are 6 times console.log on the console. (2 times at first opening - it's normal -, 2 times at clicking "Change Dogs" button, 2 times at clicking "Change Cats" button) . It means 6 times re-rendering happened.
2. Scenario: I have used reselect middleware to prevent unnecessary re-renderings. But, It doesn't work or I have misunderstood meaning of reselect.
Could someone explain the correct way or where am I doing wrong?
You are defining the selectors inside your component. You should do it outside (e.g. somewhere near your reducer).
Currently you are recreating the selector after each render. Here's a better way:
// inside reducer.js
export const petsSel = state => state.pets;
export const dogsDataMemo = createSelector(
petsSel,
pets => pets.dogs
);
export const catsDataMemo = createSelector(
petsSel,
pets => pets.cats
);
Added a codesandbox with a working example based on your code: https://codesandbox.io/s/delicate-snowflake-5ssrw
To achieve what you desire you need to use React.memo (https://reactjs.org/docs/react-api.html#reactmemo) as well:
const Dogs = React.memo(({ dogsData }) => {
console.log("Dogs rendering..");
return <h1>{dogsData}</h1>;
});
First of all, I thank #tudor very much for his efforts. All of his said is correct.
But, I want to show that Reselect works.
SCENARIO 1 - NON-MEMOIZED
import React, { memo } from "react";
import { useSelector, useDispatch } from "react-redux";
// import { catsDataMemo, dogsDataMemo } from "./selectors";
// Components
const Dogs = memo(({ dogsData }) => {
console.log("Dogs rendering..");
return <h1>{dogsData}</h1>;
});
const Cats = memo(({ catsData }) => {
console.log("Cats rendering..");
return <h1>{catsData}</h1>;
});
function ReduxDeneme() {
// Standart useSelector without MEMOIZED
const dogsData = useSelector(
state => state.pets.dogs,
console.log("dogsData Selector çalıştı.")
);
const catsData = useSelector(
state => state.pets.cats,
console.log("catsData Selector çalıştı.")
);
// Actions
const dispatch = useDispatch();
const changeDogs = () => dispatch({ type: "CHANGE_DOGS" });
const changeCats = () => dispatch({ type: "CHANGE_CATS" });
const noChangeCats = () =>
dispatch({ type: "NO_CHANGE_CATS", payload: catsData });
return (
<div>
<Dogs dogsData={dogsData} />
<Cats catsData={catsData} />
<button onClick={changeDogs}>Change Dogs</button>
<button onClick={changeCats}>Change CATS</button>
<button onClick={noChangeCats}>No Change</button>
</div>
);
}
export default memo(ReduxDeneme);
Be Careful! When you click the "Change Dogs" button, output in the console will be:
dogsData Selector çalıştı.
catsData Selector çalıştı.
Dogs rendering..
or when you click the "Change Cats" button, the output will be:
dogsData Selector çalıştı.
catsData Selector çalıştı.
Cats rendering..
Whatever button you press, both of useSelectors will work as you can see from console.log
SCENARIO 2 - MEMOIZED WITH RESELECT MIDDLEWARE
Firstly, we separate memoized selectors to another file as #tudor.gergely mentioned.
BE CAREFULL! You must define the correct path of an object.
// selectors.js
import { createSelector } from "reselect";
export const dogsDataMemo = createSelector(
state => state.pets.dogs, // BE CAREFULL while defining..
dogs => {
console.log("DogsDataMemo has worked.");
return dogs;
}
);
export const catsDataMemo = createSelector(
state => state.pets.cats, // BE CAREFULL while defining..
cats => {
console.log("CatsDataMemo has worked.");
return cats;
}
);
Then, we import this file into main.js file and use again useSelector with our memoized selectors:
import React, { memo } from "react";
import { useSelector, useDispatch } from "react-redux";
import { catsDataMemo, dogsDataMemo } from "./selectors";
// Components
const Dogs = memo(({ dogsData }) => {
console.log("Dogs rendering..");
return <h1>{dogsData}</h1>;
});
const Cats = memo(({ catsData }) => {
console.log("Cats rendering..");
return <h1>{catsData}</h1>;
});
function ReduxDeneme() {
const dogsData = useSelector(dogsDataMemo);
const catsData = useSelector(catsDataMemo);
// Actions
const dispatch = useDispatch();
const changeDogs = () => dispatch({ type: "CHANGE_DOGS" });
const changeCats = () => dispatch({ type: "CHANGE_CATS" });
const noChangeCats = () =>
dispatch({ type: "NO_CHANGE_CATS", payload: catsData });
return (
<div>
<Dogs dogsData={dogsData} />
<Cats catsData={catsData} />
<button onClick={changeDogs}>Change Dogs</button>
<button onClick={changeCats}>Change CATS</button>
<button onClick={noChangeCats}>No Change</button>
</div>
);
}
export default memo(ReduxDeneme);
and the final output:
When click the "Change Dogs" button:
DogsDataMemo has worked.
Dogs rendering
When click the "Change Cats" button:
CatsDataMemo has worked.
Cats rendering..
Related
I have a situation here where there are two components. the first component have an input field and the other component is displaying icon and a title name. i used Redux toolkit where when the input field is written anything the icon is changed to another icon. the problem is the icon doesn't change i don't why so, i want to know how to define the icon in the component as shown in my code below.
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
icon: "unTick",
value: "",
};
const tickSlice = createSlice({
name: "tickSign",
initialState,
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
changeIcon: (state) => {
if (state.icon === "unTick") {
state.icon = "tick";
} else {
state.icon = "unTick";
}
},
},
});
export const { changeIcon, setValue } = tickSlice.actions;
export const selectValue = (state) => state.value;
export const selectIcon = (state) => state.icon;
export default tickSlice.reducer;
the code of the first component of input
const value = useSelector(selectValue);
const dispatch = useDispatch();
const handleChange = useCallback(
(e) => {
dispatch(setValue(e.target.value));
if (e.target.value.trim().length !== 0) {
dispatch(changeIcon("tick"));
} else {
dispatch(changeIcon("unTick"));
}
},
[dispatch]
);
<input
id="boxInput"
type="text"
value={value}
placeholder="Enter Apprenticeship Title"
onChange={handleChange}
/>
the code for the icon change component
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { selectIcon } from "../features/TickSlice";
import { TickCircle, UnTickCircle } from "./IconSvg";
const DescNavTitles = ({ title }) => {
const icon = useSelector(selectIcon);
const dispatch = useDispatch();
return (
<div className="navTitle">
<svg>{icon === "tick" ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
};
export default DescNavTitles;
So, i don't know where exactly should i define the icon to be displayed and changed dynamically
components on the page
Redux tool when written
Redux tool when not written
You are not selecting the icon state correctly. Based on the screen captures showing the Redux state from the devtools your root state has two properties, appren and tick.
The Redux state:
{
appren: {
boxItems: [....],
title: "",
position: "",
...
},
tick: {
icon: "....",
value: "...."
},
}
I'm assuming it's this state.tick that is the tickSign slice you are working with. Recall that the useSelector hook callback is passed the entire Redux state object.
const icon = useSelector(state => state.tick.icon);
The selectValue and selectIcon selector functions need to access the correct path to access the expected state properties.
export const selectValue = (state) => state.tick.value;
export const selectIcon = (state) => state.tick.icon;
Additionally, the changeIcon action is only toggling the state value when dispatched, but it seems you want to conditionally set the value from an input element's onChange handler. I suggest the following refactor:
const initialState = {
isTicked: false,
value: "",
};
const tickSlice = createSlice({
name: "tickSign",
initialState,
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
changeIsTicked: (state, action) => {
state.isTicked = action.payload;
},
},
});
export const { changeIcon, setValue } = tickSlice.actions;
export const selectValue = (state) => state.tick.value;
export const selectIsTicked = (state) => state.tick.isTicked;
const value = useSelector(selectValue);
const dispatch = useDispatch();
const handleChange = useCallback((e) => {
dispatch(setValue(e.target.value));
dispatch(changeIsTicked(e.target.value.trim().length !== 0));
}, [dispatch]);
<input
id="boxInput"
type="text"
value={value}
placeholder="Enter Apprenticeship Title"
onChange={handleChange}
/>
const DescNavTitles = ({ title }) => {
const isTicked = useSelector(selectIsTicked);
return (
<div className="navTitle">
<svg>{isTicked ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
};
The isTicked state is actually unnecessary though as it can be completely derived from the state.tick.value state value. You can create a selector function that computes this. Example:
const initialState = {
value: "",
};
const tickSlice = createSlice({
name: "tickSign",
initialState,
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
},
});
export const { changeIcon, setValue } = tickSlice.actions;
export const selectValue = (state) => state.tick.value;
export const selectIsTicked = (state) => !!state.tick.value.length;
const DescNavTitles = ({ title }) => {
const isTicked = useSelector(selectIsTicked);
return (
<div className="navTitle">
<svg>{selectIsTicked ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
};
In the component's render method, you are using the useSelector hook to get the current icon value from the state and conditionally render either TickCircle or UnTickCircle component based on the icon value.
const icon = useSelector(selectIcon);
return (
<div className="navTitle">
<svg>{icon === "tick" ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
Try this.
My problem is when I have more than one object in the state every item of the list re-renders and they each have a deletion timeout (I want them to be deleted after an amount of time) which is reset at each re-render.
I tried to use React.memo() and React.useCallback() to prevent the item in the list to re-render when adding new items but since I recreate the array each time I add or remove a todo the values passed to list items are renewed and the items re-render.
I tried like in the code example to pass as props only values of the items and not an entire object but that not changed anything
This is a gif to illustrate my problem, but you have a code Sandbox too (just below) to see by yourself
In the first part of the video everything is going well
but when I click 2 times I expect the items to get deleted 3s after getting rendered, but only the last item gets deleted then the list disappears then the first is deleted
Here is a code Sandbox with the code below inside
index.js
import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";
const App = () => {
const [todos, setTodos] = useState([]);
const addTodo = () => {
setTodos((todos) => [
...todos,
{
id: new Date().getTime(),
message: new Date().getSeconds()
}
]);
};
const deleteTodo = (todoID) => {
const newTodos = todos.filter((todo) => todo.id !== todoID);
setTodos((t) => [...newTodos]);
};
return <Todos todos={todos} addTodo={addTodo} deleteTodo={deleteTodo} />;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
Todos.js
import Todo from "./todo";
const Todos = ({ todos, addTodo, deleteTodo }) => {
console.log("\nTodoList render");
return (
<>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return (
<Todo
key={index}
todomessage={todo.message}
id={todo.id}
deleteTodo={deleteTodo}
/>
);
})}
<button onClick={addTodo}>Add Todo</button>
</>
);
};
export default Todos;
todo.js
import { useEffect } from "react";
const Todo = ({ todomessage, id, deleteTodo }) => {
useEffect(() => {
console.log("todo " + todomessage + " rendered");
setTimeout(() => {
deleteTodo(id);
console.log("todo " + todomessage + " rendered 3s ago");
}, 3000);
});
return <p>{todomessage}</p>;
};
export default Todo;
Move the declaration of newTodos inside the setTodos callback and use the todos parameter instead of the todos variable from the useState call (since it will end up being stale due to the setTimeout):
const deleteTodo = (todoID) => {
setTodos((todos) => {
const newTodos = todos.filter((todo) => todo.id !== todoID);
return newTodos
});
}
I'm learning react context and while developing a todo application using useContext, I'm facing an issue where on submitting one task, the same task gets added two times to an array. The output component would loop through this array and display the results. While debugging I observed that, although the submit of task add only one entry into the array, not sure why and how, the consumer component gets the array with duplicate entry. Please let me know, what I'm missing.
Here is my code of index file that maintains context
import { createContext, useReducer } from "react";
import ContextReducer, { initialState } from "./ContextReducer";
const taskContext = createContext();
const ContextProvider = (props) => {
const [state, dispatch] = useReducer(ContextReducer, initialState);
const setTaskInput = (taskInput) => {
dispatch({
type: "SET_TASKINPUT",
payload: taskInput,
});
};
const addTask = (task) => {
dispatch({
type: "ADD_TASK",
payload: task,
});
};
const deleteTask = (id) => {
dispatch({
type: "DELETE_TASK",
payload: id,
});
};
const todoContext = {
todo: state.todo,
taskInput: state.taskInput,
setTaskInput,
addTask,
deleteTask,
};
return (
<taskContext.Provider value={todoContext}>
{props.children}
</taskContext.Provider>
);
};
export { taskContext };
export default ContextProvider;
This is the code for reducer
const initialState = {
todo: [],
taskInput: "",
};
const ContextReducer = (state = initialState, action) => {
if (action.type === "SET_TASKINPUT") {
state.taskInput = action.payload;
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "ADD_TASK") {
state.todo = [...state.todo, action.payload];
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "DELETE_TASK") {
state.todo = state.todo.filter((todo) => todo.id !== action.payload);
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
return state;
};
export { initialState };
export default ContextReducer;
This is the code of output component or say, consumer component
import React, { Fragment, useContext } from "react";
import { taskContext } from "../../Context";
import styles from "./Content.module.css";
const Output = () => {
const { todo, deleteTask } = useContext(taskContext);
const deleteHandler = (e) => {
deleteTask(+e.target.parentElement.parentElement.id);
};
return (
<Fragment>
{todo.length > 0 && (
<div className={styles.outputDiv}>
<ul>
{todo.map((task) => {
return (
<li key={task.id} id={task.id}>
<div className={styles.row1}>{task.task}</div>
<div className={styles.row2}>
<button className={styles.edit}>Edit</button>
<button className={styles.delete} onClick={deleteHandler}>
Delete
</button>
</div>
</li>
);
})}
</ul>
</div>
)}
</Fragment>
);
};
export default Output;
I am trying to call a function from a different component but when I console.log('hi') it appear but it didn't call the messageContext.
Here is my follwing code from Invitees.js:
const [showPreview, setShowPreview] = useState(false);
const toggleUserPreview = () => {
setShowPreview(!showPreview);
};
{showPreview && (
<ResultsWrappers togglePreview={toggleUserPreview}>
<UserPreview
userInfo={applicant}
skillStr={applicant.Skills}
togglePreview={toggleUserPreview}
/>
</ResultsWrappers>
)}
Here is the component have the function I want to call UserPreview.js:
import { useMessageContextProvider } from "../context/MessageContext";
const UserPreview = ({ userInfo, skillStr, togglePreview }) => {
const messageContextProvider = useMessageContextProvider();
const messageUser = () => {
togglePreview();
messageContextProvider.updateActiveUserToMessage(userInfo);
console.log('hi');
};
...
};
Here is my messageContext:
import { createContext, useContext, useState } from "react";
const messageContext = createContext();
export const MessageContextProvider = ({ children }) => {
const [activeUserToMessage, setActiveUserToMessage] = useState({});
const [isOpenMobileChat, toggleMobileChat] = useState(false);
const updateActiveUserToMessage = (user) => {
setActiveUserToMessage(user);
};
return (
<messageContext.Provider
value={{
updateActiveUserToMessage,
activeUserToMessage,
isOpenMobileChat,
toggleMobileChat,
}}
>
{children}
</messageContext.Provider>
);
};
export const useMessageContextProvider = () => {
return useContext(messageContext);
};
When the messageContext called it should open the chatbox like this:
The code you showing is not enough to say it for 100%, but it seems like toggleUserPreview - function called twice, so it reverted to original boolean value.
One time as <ResultsWrappers togglePreview={toggleUserPreview}/>
and second time as <UserPreview togglePreview={toggleUserPreview}/>.
I have to use useDispatch() for my toggle buttons so I have to refractor them from react to redux state. I was following the tutorial of basics of Redux and I think I have done that properly but when I try to at least useSelector to display the redux'state of button it doesnt show anything.
So here is my code:
// types.js in actions folder
export const TOGGLE = "TOGGLE";
// buttonActions in actions folder
export const toggle = () => {
return {
type: 'TOGGLE'
};
};
// buttonReducer in reducers folder
const buttonReducer = (state = true, action) => {
switch(action.type) {
case 'TOGGLE':
return !state;
default:
return state;
};
};
export default buttonReducer;
And the buttonReducer is imported into combineReducers which go to store.
The component code:
import React, { useState, useEffect } from 'react'
import isloff from './mainpage_imgs/isloff.png'
import islon from './mainpage_imgs/islon.png'
import PropTypes from "prop-types";
import { connect, useDispatch, useSelector } from "react-redux";
import { toggle } from '../../actions/buttonActions'
const Islbutton = props => {
const [open, setOpen] = useState(true);
const [role, setRole] = useState('');
useEffect(() => {
if (props.auth.user)
{
setRole(props.auth.user.role);
}
}, []);
const test = useSelector(state => state.button);
const checkRole = (role) => {
if (role === 'Menager' || role === 'Technolog')
{
return true }
else
{
return false
};
}
const toggleImage = () => {
if(checkRole(role)) {
setOpen(!open)
};
}
const getImageName = () => open ? 'islOnn' : 'islOfff'
const dispatch = useDispatch();
return(
<div>
<img style={islplace} src={open ? islon : isloff }
onClick={()=> dispatch(toggle())} />
</div>
);
}
Islbutton.propTypes = {
button: PropTypes.func.isRequired,
auth: PropTypes.obj.isRequired
};
const mapStateToProps = state => ({
button: state.button,
auth: state.auth
});
export default connect(mapStateToProps, {}), (Islbutton);
Based on your latest comments and my understanding of your use case I may suggest following distilled approach:
//dependencies
const { render } = ReactDOM,
{ createStore } = Redux,
{ connect, Provider } = ReactRedux
//action creators
const SET_ROLE = 'SET_ROLE',
MANAGER_APPROVED = 'MANAGER_APPROVED',
setRole = role => ({type:SET_ROLE, role}),
mngAppr = () => ({type:MANAGER_APPROVED})
//initial state, reducer, store
const initialState = {role:'Technolog', approved:false},
appReducer = (state=initialState, action) => {
switch(action.type){
case SET_ROLE : {
const {role} = state,
{role: newRole} = action
return {...state, role: newRole}
}
case MANAGER_APPROVED : {
const {approved} = state
return {...state, approved: !approved}
}
default: return state
}
},
store = createStore(appReducer)
//ui component to emulate toggling roles
const SwitchRoles = ({currentRole, switchRole}) => (
<div>
<label><input type="radio" name="role" value="Manager" onChange={e => switchRole(e.target.value)} />Manager</label>
<label><input type="radio" name="role" value="Technolog" onChange={e => switchRole(e.target.value)} />Technolog</label>
</div>
)
//connect radio buttons click to togling roles action
const mapDispatch = dispatch => ({switchRole: role => dispatch(setRole(role))}),
SwitchRolesContainer = connect(null,mapDispatch)(SwitchRoles)
//ui component to toggle 'approved' within global state
const ToggleApprove = ({onApprove,isManager}) => (
<button onClick={onApprove} disabled={!isManager}>Toggle</button>
)
//connect onToggle handler to dispatching 'toggle' action
const mapStateToProps = ({role}) => ({isManager: role == 'Manager'}),
mapDispatchToProps = dispatch => ({onApprove: () => dispatch(mngAppr())}),
ToggleApproveContainer = connect(mapStateToProps, mapDispatchToProps)(ToggleApprove)
//ui component to display current state of 'open'
const IsApproved = ({isApproved}) => <div>{isApproved ? 'Approved by manager' : 'Not approved by manager'}</div>
//attach isOpen prop to global 'open' variable
const mapState = ({approved}) => ({isApproved: approved}),
IsApprovedContainer = connect(mapState)(IsApproved)
//render the app
render (
<Provider store={store}>
<SwitchRolesContainer />
<IsApprovedContainer />
<ToggleApproveContainer />
</Provider>,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.1.3/react-redux.min.js"></script><div id="root"></div>
Hopefully, it gives a piece of mind about toggling global variables and mapping their values onto local components state.