Imagine a scenario that there's a same operation through different values (For instance generating a custom html element for different input values); in the case of using a component class, I would mocked this as follow:
const onFuncClicked = property => newVal => {
this.setState({ [property]: newVal })
}
but what if i use react hooks:
const onFuncClicked = property => newVal => {
eval(`set${property}(${newVal})`);
}
not only using eval is not recommended for thousands of reasons, but this code does not work at all!! It generates the correct useState function but the component does not know it even and gives a ReferenceError that that function (generated useState) is not defined
One way to approach this is to use a map of methods to the property names:
const [property, setProperty] = useState(defaultValue);
const methodMap = { propetryName: setPropertyName, /* ... more fields */ };
Then you can call it from your set method like so:
const onClicked = (property, value) => {
methodMap[property](value);
}
Another option, and probably more common one, would be to have state as an object with all your properties and change them by prop name:
const [state, setState] = useState({property: value});
const onClicked = (property, value) => {
setState(state => {...state, [property]: value});
}
well, if you are already using hooks and useState and did end up with a bunch of them that make your code look too complex I suggest using useReducer, here is a sample from official documentation:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}```
Related
I am new to using "#reduxjs/toolkit" (version "^1.5.1").
I am trying to remove an object from within the state's array (roundScore). This is usually something that is very simple to do using filter(). For some reason this isn't working and I can't figure out why. Here's my code:
Reducer slice:
import { createSlice } from "#reduxjs/toolkit";
export const roundScoreSlice = createSlice({
name: "roundScore",
initialState: {
roundScore: [],
},
reducers: {
deleteArrow: (state, action) => {
console.log(`action.payload = ${action.payload}`); // returns correct id
state.roundScore.filter((arrow) => arrow.id !== action.payload);
},
},
});
export const { deleteArrow } = roundScoreSlice.actions;
export default roundScoreSlice.reducer;
React component:
import React from "react";
import styled from "styled-components";
import { motion } from "framer-motion";
import { useDispatch } from "react-redux";
import { deleteArrow } from "../../redux-reducers/trackSession/roundScoreSlice";
export default function InputtedScore({
arrowScore,
id,
initial,
animate,
variants,
}) {
const dispatch = useDispatch();
const applyStyling = () => {
switch (arrowScore) {
case 0:
return "miss";
case 1:
return "white";
case 2:
return "white";
case 3:
return "black";
case 4:
return "black";
case 5:
return "blue";
case 6:
return "blue";
case 7:
return "red";
case 8:
return "red";
case 9:
return "gold";
case 10:
return "gold";
default:
return null;
}
};
return (
<ParentStyled
id={id}
initial={initial}
animate={animate}
variants={variants}
onClick={() => dispatch(deleteArrow(id))}
>
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
The state after adding 2 arrows would look like this:
roundScore: [
{
id:"e0f225ba-19c2-4fd4-b2bf-1e0aef6ab4e0"
arrowScore:7
},
{
id:"2218385f-b37a-4f2c-a8db-4e7e65846171"
arrowScore:5
}
]
I've tried a combination of things.
Using e.target.id within dispatch
Using e.currentTarget.id within dispatch
Using ({id}) instead of just (id) within dispatch
Wrapping the reducer function with or without braces e.g. within (state, action) => { /* code */ }
What is it I'm missing? I know this is going to be a simple fix but for some reason it's eluding me.
Any help is much appreciated.
Okay, it looks like the issue is in the way how filter method works, it returns a new array, and an initial array is not mutated (that's why we have been using filter before to manipulate redux state), also in the code you've shown value of the filtering operation not assigned to any property of your state
You need to assign the value or mutate array, so the code below should work for you
state.roundScore = state.roundScore.filter((arrow) => arrow.id !== action.payload);
Mutate your existing array:
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
We can think outside of the box and look at it from another way. the React component above is just actually a child of a certain parent component. And for the purpose of my answer i assume that in your parent component you have some form of array.map .
So from that code, each array item will already have an array index. and you can pass that index as a prop to the above react component like so:
const InputtedScore = ({ ...all your props, id, inputIndex }) => {
const dispatch = useDispatch();
// Having access to your index from the props, you can even
// find the item corresponding to that index
const inputAtIndex_ = useSelector(state => state.input[inputIndex])
const applyStyling = () => {
switch (arrowScore) {
...your style logic
}
};
return (
// you can send that index as a payload to the reducer function
<ParentStyled id={id} onClick={() => dispatch(deleteArrow(inputIndex))}
...the rest of your properties >
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
After dispaching the delete action by sending as payload the item's index already, you do not need to find the item in the reducer anymore:
deleteMeal: (state, action) => {
// you receive you inputIndex from the payload
let { inputIndex } = action.payload;
// and you use it to splice the desired item off the array
state.meals.splice(inputIndex, 1);
...your other logic if any
},
You need to mutate your existing array
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
I have a modal component in my React Native mobile app. It receives an array of objects from Redux state. I can delete a specific item in the array using dispatching an action using useDispatch hook. However, after sending the delete action, the component state is not updated automatically, so that I have to reopen the modal every time to see the updated list.
How can I set the modal to automatically re-render when the redux state is changed using dispatch?
SelectedItems.js
const SelectedItems = () => {
const vegetables = useSelector(state => state.new_order.vegetables)
return (
<Modal visible={isVisible}>
{vegetables.map( (v,index) =>
<VegeItem
key={index}
index={index}
name={v.name}
qty={v.qty}
metric={v.metric}
removeItem={(index) => {
dispatch({
type: 'DELETE_VEGE',
id: index
})
}}
/>)}
</View>
</Modal>
)
}
newOrderReducer.js
const newOrderReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'ADD_VEGE':
let updatedList = [...state.vegetables,action.vege]
return {
...state,
vegetables: updatedList
}
case 'DELETE_VEGE':
let newVegeList = state.vegetables
newVegeList.splice(action.id,1)
return {
...state,
vegetables: newVegeList
}
default:
return state
}
};
while doing like so let newVegeList = state.vegetables, newVegeList is just a pointer on your state and not a shallow copy of it. Therefore, you still can't mutate it as you can't mutate state outside the return part of the reducer.
so you can do like let newVegeList = [...state.vegetables], or directly at the return
return {
...state,
vegetables: state.vegetables.filter((veg, i) => i != action.id)
}
you can also send veg name or whatever and modify the checker at filter
I'm pretty new to React and React hooks in general,
I'm building a react app for my final project and I wanted to make some component (Advanced search in this example) as generalized as possible which means I want to pass "dataFields" and the component should be updated with a unique state value that originated from those data fields.
I know that I can use a general state and store changes in it with an array but I read that it's bad practice.
this is what I have now:
const [title,updateTitle] = useState({"enable":false,"value": "" });
const [tags,updateTags] = useState({"enable":false,"value": "" });
const [owner,updateOwner] = useState({"enable":false,"value": "" });
const [desc,updateDesc] = useState({"enable":false,"value": "" });
And I try to use this to achieve the same thing:
if(props?.dataFields) {
Object.entries(props.dataFields).forEach ( ([key,value]) => {
// declare state fields
const [key,value] = useState(value)
});
}
what is the proper way of doing it? is there is one?
Do 4 lines of useState or useReducer (local)
I would suggest someting like this for the initial state
const setItem = (enable = false, value = '') => ({ enable, value });
const [title, updateTitle] = useState(setItem());
const [tags, updateTags] = useState(setItem());
const [owner, updateOwner] = useState(setItem());
const [desc, updateDesc] = useState(setItem());
And you also can useReducer and define the initial state.
I add an example for useReducer and case dor change title.value
import React from 'react';
import { useReducer } from 'react';
const setItem = (enable = false, value = '') => ({ enable, value });
const initialState = { title: setItem(), tags: setItem(), owner: setItem(), desc: setItem() };
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_TITLE':
return { ...state, title: setItem(null, action.payload) };
default:
return state;
}
}
function MyFirstUseReducer() {
const [state, dispatch] = useReducer(reducer, initialState);
const updateTitle = ev => {
if (ev.which !== 13 || ev.target.value === '') return;
dispatch({ type: 'CHANGE_TITLE', payload: ev.target.value });
ev.target.value = '';
};
return (
<>
<h2>Using Reducer</h2>
<input type="text" onKeyUp={updateTitle} placeholder="Change Title" />
<div>
<span>The State Title is: <strong>{state.title.value}</strong></span>
</div>
</>
);
}
export default MyFirstUseReducer;
I have two states:
const [firstState, setFirstState] = useState(6.5)
const [secondState, setSecondState] = useState(3.5)
I wish to combine these two states into an object, like this:
const [myObject, setMyObject] = useState({
first: {firstState},
second: {secondState}
});
//NOTE: This does not compile when i try to create state-object with state values.
I then send the object to a child-component as a prop which is later used for display.
<ChildComponent objectToChild={myObject}/>
What's the best pratice to combine states into an object?
Their are different option
1. Using the useState Hook
const [myObject, setMyObject] = useState({
first: firstState,
second: secondState
});
modify state
setMyObject({...myObject, firstState })
2. Using the useReducer
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
const initialState = {
first: 'name',
second: 'surname'
};
const reducer = (state, action) => {
switch (action) {
case 'first': return { ...state, first: state.first };;
case 'second': return { ...state, second: state.second };;
default: throw new Error('Unexpected action');
}
};
use it like
const [state, dispatch] = useReducer(reducer, initialState);
You can simply create an object as an argument in useState method, so It would be like this:
const [user, setUser] = useState({});
and call setUser method like this:
setUser(prevState => {
return {...prevState, [user.firstState]: 6.5}
}
Read more about useState hook here:
https://pl.reactjs.org/docs/hooks-reference.html#usestate
I have stored an array of object in Redux State and inside each object there is a key named price. Now when I increment the quantity button I need to access the object that has the key inside redux and change the price value. I was able to do that but it's not working properly the price is being changed but a new object is being added in the state of Redux you can see it in the screenshot below. hope I was able to explain the problem clearly. if not please let know so I can explain more.
Cart Component
increment(e, item){
let qty = e.target.previousElementSibling.textContent;
qty++;
e.target.previousElementSibling.textContent = qty;
this.props.changePrice(item);
}
<div>
<input onClick={(e) =>this.decrement(e)} type="submit" value="-"/>
<span>1</span>
<input onClick={(e) => this.increment(e, item)} type="submit" value="+"/>
</div>
function mapStateToProps(state){
return({
itemlist: state.rootReducer
})
}
function mapDispatchToProps(dispatch) {
return({
removeItem: (item)=>{
dispatch({type: 'removeCart', payload: item})
},
changePrice: (item)=>{
dispatch({type: 'changePrice', payload: item})
}
})
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart);
Reducer Component
const changePrice = (itemArray, item)=>{
let newObject = {};
let filteredObject = itemArray.filter(reduxItem => reduxItem.id === item.id);
let newprice = filteredObject[0].price + filteredObject[0].price;
filteredObject[0].price = newprice;
newObject = filteredObject;
const something = ([...itemArray, newObject]);
return something;
}
const reducer = (state = [], action) =>{
switch (action.type) {
case 'Add':
return [...state, action.payload]
case 'removeCart':
const targetItemIndex = state.indexOf(action.payload);
return state.filter((item, index) => index !== targetItemIndex)
case 'changePrice':
return changePrice(state, action.payload)
default:
return state;
}
}
export default reducer;
filteredObject is an array. You override the newObject to be an array in this statement newObject = filteredObject. So the newObject is an array ( in [...itemArray, newObject] ) rather than an object. Keep things simple without unnecessary complexity.You can use Array.map. So do this instead
const changePrice = (itemArray, item) => {
return itemArray.map(reduxItem => {
if(reduxItem.id === item.id){
reduxItem.price = reduxItem.price + reduxItem.price
}
return reduxItem
});
};
See this for more info https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#inserting-and-removing-items-in-arrays
Hope this helps!
Instead of mutating the state.
// use this
const newState = Object.assign({},state);
We can create a new state and now if you do this, this works fine.
This avoids mutating state.