I have the below code:
import ReactDOM from "react-dom";
const initialTodos = [
{
id: 1,
title: "Todo 1",
complete: false,
},
{
id: 2,
title: "Todo 2",
complete: false,
},
];
const reducer = (state, action) => {
switch (action.type) {
case "COMPLETE":
return state.map((todo) => {
if (todo.id === action.id) {
return { ...todo, complete: !todo.complete };
} else {
return todo;
}
});
default:
return state;
}
};
function Todos() {
const [todos, dispatch] = useReducer(reducer, initialTodos);
const handleComplete = (todo) => {
dispatch({ type: "COMPLETE", id: todo.id });
};
return (
<>
{todos.map((todo) => (
<div key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.complete}
onChange={() => handleComplete(todo)}
/>
{todo.title}
</label>
</div>
))}
</>
);
}
ReactDOM.render(<Todos />, document.getElementById("root"));
I am trying to test this component with jest. How to mock the useReducer hook here so that I can change the state of the component manually rather than changing it by clicking the checkbox.
I have tried some examples but unfortunately it did not work. Please suggest.
Related
Hi I'm new to REACT and I have a HW where I need to create a grocery shopping list and I need to create a clear button. The isPurchased key value pair is a boolean though. I need to create a button that when I click Purchased it clears that grocery item off my list. Any help would be appreciated.
class App extends Component {
state = {
grocery: grocery,
item: '',
brand: '',
units: Number,
quantity: Number,
isPurchased: Boolean
}
handleChange = (e) => {
this.setState({ [e.target.id]: e.target.value })
}
handleSubmit = (e) => {
e.preventDefault()
const addGrocery = {
item: this.state.item,
brand: this.state.brand,
units: this.state.units,
quantity: this.state.quantity,
}
this.setState({
grocery: [addGrocery, ...this.state.grocery],
item: '',
brand: '',
units: Number,
quantity: Number,
})
const removeGrocery = {
item: this.state.item
}
}
hey here is a full code for creating a to do list in react (it will be very similar to your problem):
**
Summary
** of the idea of creating a to-do list or shopping list is that each to-do will be an object, when we create a new object we will insert it into an array. once it is in the array by using the array.map() function we will convert each object to an HTML element to make the UI.
if something is unclear I am here to answer
file - App.js:
import React, { useState, useReducer } from "react";
import Todo from "./Todo";
export const ACTIONS = {
ADD_TODO: "add-todo",
TOGGLE_TODO: "toggle-todo",
DELETE_TODO: "delete-todo",
};
function reducer(todos, action) {
switch (action.type) {
case ACTIONS.ADD_TODO:
return [...todos, newTodo(action.payload.name)];
case ACTIONS.TOGGLE_TODO:
return todos.map((todo) => {
if (todo.id === action.payload.id) {
return { ...todo, complete: !todo.complete }; //change to complete if we found to id that toggled
}
return todo;
});
case ACTIONS.DELETE_TODO:
return todos.filter((todo) => todo.id !== action.payload.id);
default:
return todos;
}
}
function newTodo(name) {
return { id: Date.now(), name: name, complete: false };
}
const App = () => {
const [todos, dispatch] = useReducer(reducer, []); //useReducer return the state and the reducer function
const [name, setName] = useState("");
function handleSubmit(e) {
e.preventDefault();
dispatch({ type: ACTIONS.ADD_TODO, payload: { name: name } });
setName("");
}
return (
<>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
{todos.map((todo) => {
return <Todo key={todo.id} todo={todo} dispatch={dispatch} />;
})}
</>
);
};
export default App;
Another file (component) - Todo.js:
import React from "react";
import { ACTIONS } from "./App";
const Todo = ({ todo, dispatch }) => {
return (
<div>
<span style={{ color: todo.complete ? "#AAA" : "#000" }}>
{todo.name}
</span>
<button
onClick={() =>
dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })
}
>
Toggle
</button>
<button
onClick={() =>
dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })
}
>
Delete
</button>
</div>
);
};
export default Todo;
In this problem what can I do so that the on clicking the button, both the function add to the cart and select the checkbox executes together? In the current scenario add to cart is working when the button is clicked but the checkbox isn't selected. I removed all the styles so that the actual code is readable
YourItems.js
import React from "react";
import { connect } from "react-redux";
import { addOn } from "./data";
import { addOnhandleChange,addOnSelector} from "./AddOnActions";
const YourItems = ({ addOnhandleChange, addOnSelector, selectedId }) => {
return (
<div>
{addOn.map(({ id, name, img, price, unit }) => {
return (
<div key={id}>
<div>
<img src={img} alt={name} />
<p>{name}</p>
<span>Rs. {price}</span>
<input
type="checkbox"
checked={id === selectedId}
onChange={() => addOnhandleChange(id)}
/>
</div>
<button onClick={() =>addOnSelector({id, name,img,price,unit, })}>
Add
</button>
</div>
)})}
</div>
);
};
const mapStateToProps = (state) => {
return {
selectedId: state.addOn.selectedId,
};
};
export default connect(mapStateToProps, { addOnSelector,addOnhandleChange})(YourItems);
AddOnAction.js
export const addOnhandleChange = (id) => (dispatch) => {
dispatch({
type: "SELECTED_ID",
payload: id,
});
};
export const addOnSelector = ({ id, name, img, price, unit }) => (dispatch) => {
dispatch({
type: "ADD_TO_CART",
payload: { id, name, img, price, unit },
});
};
reducer.js
const initialState = {
selectedId: "",
};
export default function SelectorReducer(
state = initialState,
action
) {
switch (action.type) {
case "SELECTED_ID":
return {
...state,
selectedId: action.payload,
};
default:
return state;
}
}
data.js
export const addOn = [
{
id: 12654,
img: "https://images.pexels.com/photos/1132047/pexels-photo-1132047.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
name: "Banana",
price: 10,
unit: 1,
},
{
id: 2256435,
img: "https://images.pexels.com/photos/1132047/pexels-photo-1132047.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
name: "Mango",
price: 20,
unit: 1,
},
{
id: 3429684,
img: "https://images.pexels.com/photos/1132047/pexels-photo-1132047.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
name: "Grape",
price: 30,
unit: 1,
},
];
Add a case for "ADD_TO_CART" action type and use the id packed in action.payload in the reducer.
export default function SelectorReducer(
state = initialState,
action
) {
switch (action.type) {
case "SELECTED_ID":
return {
...state,
selectedId: action.payload,
};
case "ADD_TO_CART":
return {
...state,
selectedId: action.payload.id, // <-- use the payload.id
};
default:
return state;
}
}
I'm try to make fields validation in mine project and here is mine fields state model
const initialState = {
fields: {
title: {
value: '',
isCorrectValue: false
},
amount: {
value: '',
isCorrectValue: false
}
}
}
I'm trying to update mine field state isCorrectValue if value.length lower then 1 here is regex what I'm using to check fields value length
const checkValue = (value) => {
return (/^.{1,}$/).test(value);
};
here is mine reducer where I'm trying to update mine state cannot understand why i cannot to grasp
isCorrectValue
export default function fieldsReducer(state = initialState, action) {
switch (action.type) {
case ONCHANGE:
return {
...state,
fields: {
...state.fields,
[`${action.payload.name}`]: {
...[`${action.payload.name}`],
value: action.payload.value
}
}
}
case VALIDATEFIELDS:
return {
...state,
fields: Object.keys(state.fields).reduce((acc, curr) => {
!checkTitle(state.fields[curr].value)
? Object.assign(acc, state.fields,
{
curr: { ...state.fields, [state.fields[curr].isCorrectValue]: !state.fields[curr].isCorrectValue }
}
)
: acc = state.fields;
return acc;
}, {})
}
default: return state;
}
}
here is mine component where reducer is working
const AddTransaction = () => {
const state = useSelector(state => state.fieldsReducer);
const dispatch = useDispatch();
console.log(state.fields.title.isCorrectValue)
return (
<div className='add-transaction-wrapper'>
<div className='add-expense-inputs-wrapper'>
<TextField
id='title'
label='title'
value={state.fields.title.value}
onChange={e => dispatch(onHandleChange(e, e.target.id))}
/>
<TextField
id="amount"
label="expense amount"
type="number"
InputLabelProps={{
shrink: true,
}}
value={state.fields.amount.value}
onChange={e => dispatch(onHandleChange(e, e.target.id))}
error={state.fields.amount.isCorrectValue}
/>
<button onClick={() => dispatch(fieldsValidation())}>click</button>
</div>
</div>
)
}
You can try the following in your reducer:
//not part of your reducer
const state = {
fields: {
title: {
value: '',
isCorrectValue: false
},
amount: {
value: '',
isCorrectValue: false
}
}
}
//just a sample action
const action = {
payload:{
name:'amount',
value:true
}
}
//this is what you should do in your reducer
const newState = {
...state,
fields: {
...state.fields,
[action.payload.name]: {
...state.fields[action.payload.name],
value: action.payload.value
}
}
}
console.log('new state:',newState)
VALIDATEFIELDS is kind of a mess and I have no idea what you want to do there.
I'm just trying to learn the useReducer hook in react. Just playing around. Can someone help with an issue I'm having? When I click the button to "plus a unit" or "minus" a unit it takes 2 away. Any suggestions?
Perhaps map. is a bad method for updating the state in the reducer?
Test code below. I've put it all into one component for purpose of below, rather than posting all the context stuff and children.
Any help much appreciated :)
import React, {useReducer} from 'react'
function Test() {
const itemFile = [
{sku: '123456', description: 'item 1', stockavailable: 5},
{sku: '654321', description: 'item 2', stockavailable: 1},
{sku: '666666', description: 'item 3', stockavailable: 0},
]
const reducer = (state,action) => {
switch (action.type) {
case 'DELETE_LINE':
let newstate = state.filter(item => item.sku !== action.payload)
return newstate
case 'MINUS_ONE_UNIT':
return state.map(item => {
if(item.sku === action.payload) {
item.stockavailable = item.stockavailable-1;
}
return item;
})
case 'PLUS_ONE_UNIT':
return state.map(item => {
if(item.sku === action.payload) {
item.stockavailable = item.stockavailable+1;
}
return item;
})
default:
return state
}
}
const [liveItemFile, dispatch] = useReducer(reducer, itemFile);
return (
<>
{liveItemFile.map((item) => (
<div key={item.sku}>
{item.sku}: {item.description} - {item.stockavailable}
<button onClick={() => dispatch({type: 'DELETE_LINE', payload: item.sku})}>
DELETE LINE
</button>
<button onClick={() => dispatch({type: 'MINUS_ONE_UNIT', payload: item.sku})}>
Minus Unit
</button>
<button onClick={() => dispatch({type: 'PLUS_ONE_UNIT', payload: item.sku})}>
Plus Unit
</button>
</div>
))}
</>
);
}
export default Test;
Move your reducer's logic and your initial state outside of your component as with each render they get evaluated:
import React, { useReducer } from "react";
const itemFile = [
{ sku: "123456", description: "item 1", stockavailable: 5 },
{ sku: "654321", description: "item 2", stockavailable: 1 },
{ sku: "666666", description: "item 3", stockavailable: 0 },
];
const reducer = (state, action) => {
switch (action.type) {
case "DELETE_LINE":
let newstate = state.filter((item) => item.sku !== action.payload);
return newstate;
case "MINUS_ONE_UNIT":
return state.map((item) => {
if (item.sku === action.payload) {
item.stockavailable = item.stockavailable - 1;
}
return item;
});
case "PLUS_ONE_UNIT":
return state.map((item) => {
if (item.sku === action.payload) {
item.stockavailable = item.stockavailable + 1;
}
return item;
});
default:
return state;
}
};
function Test() {
const [liveItemFile, dispatch] = useReducer(reducer, itemFile);
return (
<>
{liveItemFile.map((item) => (
<div key={item.sku}>
{item.sku}: {item.description} - {item.stockavailable}
<button onClick={() => dispatch({ type: "DELETE_LINE", payload: item.sku })}>DELETE LINE</button>
<button onClick={() => dispatch({ type: "MINUS_ONE_UNIT", payload: item.sku })}>Minus Unit</button>
<button onClick={() => dispatch({ type: "PLUS_ONE_UNIT", payload: item.sku })}>Plus Unit</button>
</div>
))}
</>
);
}
export default Test;
I have a simple example where I pass a clickFunction as a value to React Context and then access that value in a child component. That child component re-renders event though I'm using React.memo and React.useCallback. I have an example in stackblitz that does not have the re-render problem without using context here:
https://stackblitz.com/edit/react-y5w2cp (no problem with this)
But, when I add context and pass the the function as part of the value of the context, all children component re-render. Example showing problem here:
https://stackblitz.com/edit/react-wpnmuk
Here is the problem code:
Hello.js
import React, { useCallback, useState, createContext } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
return (
<GlobalContext.Provider
value={{
clickFunction: memoizedValue,
}}
>
{speakers.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</GlobalContext.Provider>
);
};
Speaker.js
import React, {useContext} from "react";
import { GlobalContext } from "./Hello";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
WORKING CODE BELOW FROM ANSWERS BELOW
Speaker.js
import React, { useContext } from "react";
import { GlobalContext } from "./Hello";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
Hello.js
import React, { useState, createContext, useMemo } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = (speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
};
const provider = useMemo(() => {
return ({clickFunction: clickFunction});
}, []);
return (
<GlobalContext.Provider value={provider}>
{speakers.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</GlobalContext.Provider>
);
};
when passing value={{clickFunction}} as prop to Provider like this when the component re render and will recreate this object so which will make child update, so to prevent this
you need to memoized the value with useMemo.
here the code:
import React, { useCallback, useState, createContext,useMemo } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
const provider =useMemo(()=>({clickFunction}),[])
return (
<div>
{speakers.map((rec) => {
return (
<GlobalContext.Provider value={provider}>
<Speaker
speaker={rec}
key={rec.id}
></Speaker>
</GlobalContext.Provider>
);
})}
</div>
);
};
note you dont need to use useCallback anymore clickFunction
This is because your value you pass to your provider changes every time. So, this causes a re-render because your Speaker component thinks the value is changed.
Maybe you can use something like this:
const memoizedValue = useMemo(() => ({ clickFunction }), []);
and remove useCallback from the function definition since useMemo will handle this part for you.
const clickFunction = speakerIdClicked =>
setSpeakers(currentState =>
currentState.map(rec => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
and pass this to your provider such as:
<GlobalContext.Provider value={memoizedValue}>
<Speaker speaker={rec} key={rec.id} />
</GlobalContext.Provider>
After providing the answer, I've realized that you are using Context somehow wrong. You are mapping an array and creating multiple providers for each data. You should probably change your logic.
Update:
Most of the time you want to keep the state in your context. So, you can get it from the value as well. Providing a working example below. Be careful about the function this time, we are using useCallback for it to get a stable reference.
const GlobalContext = React.createContext({});
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
function App() {
const [speakers, setSpeakers] = React.useState(speakersArray);
const clickFunction = React.useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
const memoizedValue = React.useMemo(() => ({ speakers, clickFunction }), [
speakers,
clickFunction,
]);
return (
<GlobalContext.Provider value={memoizedValue}>
<Speakers />
</GlobalContext.Provider>
);
}
function Speakers() {
const { speakers, clickFunction } = React.useContext(GlobalContext);
return speakers.map((speaker) => (
<Speaker key={speaker.id} speaker={speaker} clickFunction={clickFunction} />
));
}
const Speaker = React.memo(({ speaker, clickFunction }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />