I am learning React Reducer now. I want to build a toggle button that changes a boolean completed value to its opposite each time I click the button.
What I have is an array of states, each state is an object with an id and a completed value set to be true or false. Then I loop through states, setting each state as an Item component and display it on screen.
// App.js file
import React, { useReducer } from "react";
import { AppReducer } from "./AppReducer";
import Item from "./Item";
function App() {
const initialStates = [
{
id: 1,
completed: false,
},
{
id: 2,
completed: false,
},
];
const [states, dispatch] = useReducer(AppReducer, initialStates);
return (
<div>
{states.map((state) => (
<Item item={state} key={state.id} dispatch={dispatch} />
))}
</div>
);
}
export default App;
In the Item component, I display whether this item is completed or not (true or false). I set up a toggle function on the button to change the completed state of the Item.
// Item.js
import React from "react";
const Item = ({ item, dispatch }) => {
function setButtonText(isCompleted) {
return isCompleted ? "True" : "False";
}
let text = setButtonText(item.completed);
function toggle(id){
dispatch({
type: 'toggle',
payload: id
})
text = setButtonText(item.completed);
}
return (
<div>
<button type="button" onClick={() => toggle(item.id)}>Toggle</button>
<span>{text}</span>
</div>
);
};
export default Item;
Here is my reducer function. Basically what I am doing is just loop through the states array and locate the state by id, then set the completed value to its opposite one.
// AppReducer.js
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const newStates = states;
for (const state of newStates) {
if (state.id === action.payload) {
const next = !state.completed;
state.completed = next;
break;
}
}
return [...newStates];
}
default:
return states;
}
};
So my problem is that the toggle button only works once. I checked my AppReducer function, it did change completed to its opposite value, however, every time we return [...newStates], it turned back to its previous value. I am not sure why is that. I appreciate it if you can give it a look and help me.
The code is available here.
Here is the working version forked from your codesandbox
https://codesandbox.io/s/toggle-button-forked-jy6jd?file=/src/Item.js
The store value updated successfully. The problem is the way of listening the new item change.
dispatch is a async event, there is no guarantee the updated item will be available right after dispatch()
So the 1st thing to do is to monitor item.completed change:
useEffect(() => {
setText(setButtonText(item.completed));
}, [item.completed]);
The 2nd thing is text = setButtonText(item.completed);, it will not trigger re-render. Therefore, convert the text to state and set it when item.completed to allow latest value to be displayed on screen
const [text, setText] = useState(setButtonText(item.completed));
I have improved your code, just replace your AppReducer code with below.
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const updated = states.map((state) =>
action.payload === state.id ? {
...state,
completed: !state.completed
} : { ...state }
);
return [...updated];
}
default:
return states;
}
};
Live demo
Related
The useEffect doesn't fire on first render, but when I save the file (ctrl+s), the state updates and the results can be seen.
What I want to do is, when I'm in GameScreen, I tap on an ICON which takes me to WalletScreen, from there I can select some items/gifts (attachedGifts - in context) and after finalising I go back to previous screen i.e. GameScreen with gifts attached (attachedGifts!==null), now again when I tap ICON and go to WalletScreen it should show me the gifts that were attached so that I could un-attach them or update selection (this is being done in the useEffect below in WalletScreen), but the issue is, although my attachedGifts state is updating, the useEffect in WalletScreen does not fire immediately when navigated, when I hit ctrl+s to save the file, then I can see my selected/attached gifts in WalletScreen.
code:
const Main = () => {
return (
<GiftsProvider>
<Stack.Screen name='WalletScreen' component={WalletScreen} />
<Stack.Screen name='GameScreen' component={GameScreen} />
</GiftsProvider>
)
};
const GameScreen = () => {
const { attachedGifts } = useGifts(); //coming from context - GiftsProvider
console.log('attached gifts: ', attachedGifts);
return ...
};
const WalletScreen = () => {
const { attachedGifts } = useGifts();
useEffect(() => { // does not fire on initial render, after saving the file, then it works.
if (attachedGifts !== null) {
let selectedIndex = -1
let filteredArray = data.map(val => {
if (val.id === attachedGifts.id) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
setData(filteredArray);
}
}, [attachedGifts]);
const attachGiftsToContext = (obj) => {
dispatch(SET_GIFTS(obj));
showToast('Gifts attached successfully!');
navigation?.goBack(); // goes back to GameScreen
}
return (
// somewhere in between
<TouchableOpacity onPress={attachGiftsToContext}>ATTACH</TouchableOpacity>
)
};
context:
import React, { createContext, useContext, useMemo, useReducer } from 'react';
const GiftsReducer = (state: Object | null, action) => {
switch (action.type) {
case 'SET_GIFTS':
return action.payload;
default:
return state;
}
};
const GiftContext = createContext({});
export const GiftsProvider = ({ children }) => {
const initialGiftState: Object | null = null;
const [attachedGifts, dispatch] = useReducer(
GiftsReducer,
initialGiftState,
);
const memoedValue = useMemo(
() => ({
attachedGifts,
dispatch,
}),
[attachedGifts],
);
return (
<GiftContext.Provider value={memoedValue}>
{children}
</GiftContext.Provider>
);
};
export default function () {
return useContext(GiftContext);
}
Output of console.log in GameScreen:
attached gifts: Object {
"reciptId": "baNlCz6KFVABxYNHAHasd213Fu1",
"walletId": "KQCqSqC3cowZ987663QJboZ",
}
What could possibly be the reason behind this and how do I solve this?
EDIT
Added related code here: https://snack.expo.dev/uKfDPpNDr
From the docs
When you call useEffect in your component, this is effectively queuing
or scheduling an effect to maybe run, after the render is done.
After rendering finishes, useEffect will check the list of dependency
values against the values from the last render, and will call your
effect function if any one of them has changed.
You might want to take a different approach to this.
There is not much info, but I can try to suggest to put it into render, so it might look like this
const filterAttachedGifts = useMemo(() => ...your function from useEffect... , [attachedGitfs])
Some where in render you use "data" variable to render attached gifts, instead, put filterAttachedGifts function there.
Or run this function in component body and then render the result.
const filteredAttachedGifts = filterAttachedGifts()
It would run on first render and also would change on each attachedGifts change.
If this approach doesn't seems like something that you expected, please, provide more code and details
UPDATED
I assume that the problem is that your wallet receive attachedGifts on first render, and after it, useEffect check if that value was changed, and it doesn't, so it wouldn't run a function.
You can try to move your function from useEffect into external function and use that function in 2 places, in useEffect and in wallet state as a default value
feel free to pick up a better name instead of "getUpdatedArray"
const getUpdatedArray = () => {
const updatedArray = [...walletData];
if (attachedGifts !== null) {
let selectedIndex = -1
updatedArray = updatedArray.map((val: IWalletListDT) => {
if (val?.walletId === attachedGifts?.walletIds) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
setPurchaseDetailDialog(val);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
}
return updatedArray;
}
Then use it here
const [walletData, setWalletData] = useState(getUpdatedArray());
and in your useEffect
useEffect(() => {
setWalletData(getUpdatedArray());
}, [attachedGifts]);
That update should cover the data on first render. That might be not the best solution, but it might help you. Better solution require more code\time etc.
I have a React component that maps state to props to get data via redux. Everything works fine with the action and the value being updated properly in the reducer. My only problem is that when the state value changes, I want my component to re render so that it is always displaying the most up to date value in the reducer. As of right now I have to call a separate function that refreshes the component, but I'd rather have it automatically re render every time that value changes in the reducer.
Action:
export const createPickup = (selected, pickups) => dispatch => {
let icon;
icon = check(selected);
pickups.icon = icon;
return API('/createPickUp/', {
...pickups,
})
.then(res => {
dispatch({type: types.CREATE_PICKUP, res});
})
.catch(err => console.log(err));
};
Reducer:
const initialState = {
pick: [],
};
export default function pickup(state = initialState, action) {
switch (action.type) {
case types.GET_PICK:
return {
pick: action.pickup,
};
case types.CREATE_PICKUP:
return {
pick: [action.res, ...state.pick],
};
case types.DEL_GAME:
return {
pick: state.pick.filter(p => p._id !== action.id),
};
case types.START_GAME:
return {
pick: state.pick.map(p =>
p._id === action.id ? {...p, start: true} : p,
),
};
case types.STOP_GAME:
return {
pick: state.pick.map(p =>
p._id === action.id ? {...p, stop: true} : p,
),
};
default:
return state;
}
}
Use useSelector hook in Functional Component as it automatically subscribes to the state and your component will re-render.
If you are using Class Component then use connect() from redux and mapStateinProps.
I am assuming you have passed the reducer to the global Store.
Now... make sure you have the up to date value in your component.. try consoling it like this...
import {useSelector} from 'react-redux';
const YourCmponent = () => {
const reduxState = useSelector(state => state);
console.log(reduxState);
return <div>Your Content</div>
}
That way you can get access to the redux store. And you don't need to make any other function for updating component You will always get updated value here.
This is from a tutorial assignment from Dave Ceddia's Redux course, I am trying to display the initial state, which contains an array of objects, however it is simply returning undefined and not displaying anything. I am new to React, and I have hit a wall on getting 1) my buttons to display the state, and 2) default state to appear initially.
I have tried to have my component Buttons as a class, and constant.
I have tried stating my initialReducer in the default: return state; in my reducer as well. I have also tried different syntax for my dispatch actions, but nothing seems to be getting to the reducer.
index.js
import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import { getAllItems, addEventToBeginning, addEventToEnd } from "./actions";
import { connect, Provider } from "react-redux";
import { store } from "./reducers";
const Buttons = ({
state,
getAllItems,
addEventToBeginning,
addEventToEnd
}) => (
<React.Fragment>
<ul>{state ? state.actions.map(item => <li>{item}</li>) : []}</ul>
<button onClick={getAllItems}> Display items </button>
<button onClick={addEventToBeginning}> addEventToBeginning </button>
<button onClick={addEventToEnd}> addEventToEnd </button>
</React.Fragment>
);
const mapDispatchToProps = { getAllItems, addEventToBeginning, addEventToEnd };
const mapStateToProps = state => ({
actions: state.actions,
sum: state.sum
});
connect(
mapStateToProps,
mapDispatchToProps
)(Buttons);
reducers.js
const initialState = {
actions: [
{ id: 0, type: "SALE", value: 3.99 },
{ id: 1, type: "REFUND", value: -1.99 },
{ id: 2, type: "SALE", value: 17.49 }
],
sum: 0
};
const newUnit = { id: Math.random * 10, type: "SALE", value: Math.random * 25 };
function eventReducer(state = initialState, action) {
switch (action.type) {
case ADD_EVENT_TO_BEGINNING:
const copy = { ...state };
copy.actions.unshift(newUnit);
return copy;
case ADD_EVENT_TO_END:
const copy2 = { ...state };
copy2.actions.unshift(newUnit);
return copy2;
cut out for cleanliness
case GET_ITEMS:
return {
...state,
actions: state.actions,
sum: state.sum
};
default:
return state;
}
}
export const store = createStore(eventReducer);
example of actions.js (they all follow same format)
export const ADD_EVENT_TO_BEGINNING = "ADD_EVENT_TO_BEGINNING";
export function addEventToBeginning() {
return dispatch => {
dispatch({
type: ADD_EVENT_TO_BEGINNING
});
};
}
UPDATE:
Thank you #ravibagul91 and #Yurui_Zhang, I cut everything but getAllItems out, and changed the state to:
const initialState = {
itemsById: [
{ id: 0, type: "SALE", value: 3.99 },
{ id: 1, type: "REFUND", value: -1.99 },
{ id: 2, type: "SALE", value: 17.49 }
]
};
class Form extends React.Component {
render() {
return (
<div>
{this.props.itemsById
? this.props.itemsById.map(item => (
<li>
{item.id} {item.type} {item.value}
</li>
))
: []}
<button onClick={this.getAllItems}> Display items </button>
</div>
);
}
}
const mapDispatchToProps = { getAllItems };
function mapStateToProps(state) {
return {
itemsById: state.itemsById
};
}
export function getAllItems() {
return dispatch => ({
type: "GET_ITEMS"
});
}
There are multiple problems with your code:
const mapStateToProps = state => ({
actions: state.actions,
sum: state.sum
});
Here you have mapped redux state fields to props actions and sum - your component won't receive a state prop, instead it will receive actions and sum directly.
so your component really should be:
const Button = ({
actions,
sum,
}) => (
<>
<ul>{actions && actions.map(item => <li>{item}</li>)}</ul>
</>
);
your mapDispatchToProps function is not defined correctly. It should be something like this:
// ideally you don't want the function names in your component to be the same as the ones you imported so I'm renaming it here:
import { getAllItems as getAllItemsAction } from "./actions";
// you need to actually `dispatch` the action
const mapDispatchToProps = (dispatch) => ({
getAllItems: () => dispatch(getAllItemsAction()),
});
Your reducer doesn't seem to be defined correctly as well, however you should try to fix the problems I mentioned above first :)
Try not to do too much in one go when you are learning react/redux. I'd recommend reviewing the basics (how the data flow works, how to map state from the redux store to your component, what is an action-creator, etc.).
As you are destructuring the props,
const Buttons = ({
state,
getAllItems,
addEventToBeginning,
addEventToEnd
}) => ( ...
You don't have access to state, instead you need to directly use actions and sum like,
const Buttons = ({
actions, // get the actions directly
sum, // get the sum directly
getAllItems,
addEventToBeginning,
addEventToEnd
}) => (
<React.Fragment>
//You cannot print object directly, need to print some values like item.type / item.value
<ul>{actions && actions.length && actions.map(item => <li>{item.type} {item.value}</li>)}</ul>
<button onClick={getAllItems}> Display items </button>
<button onClick={addEventToBeginning}> addEventToBeginning </button>
<button onClick={addEventToEnd}> addEventToEnd </button>
</React.Fragment>
);
Your mapDispatchToProps should be,
const mapDispatchToProps = dispatch => {
return {
// dispatching actions returned by action creators
getAllItems : () => dispatch(getAllItems()),
addEventToBeginning : () => dispatch(addEventToBeginning()),
addEventToEnd : () => dispatch(addEventToEnd())
}
}
Or you can make use of bindActionCreators,
import { bindActionCreators } from 'redux'
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ getAllItems, addEventToBeginning, addEventToEnd }, dispatch)
}
}
In reducer, ADD_EVENT_TO_END should add element to end of the array, but you are adding again at the beginning using unshift. You should use push which will add element at the end of array,
case ADD_EVENT_TO_END:
const copy2 = { ...state };
copy2.actions.push(newUnit); //Add element at the end
return copy2;
Also your GET_ITEMS should be as simple as,
case GET_ITEMS:
return state;
I am learning redux using react. I am trying to update an array of numbers based on a button click. I am specifically want to update the counter at specific index based on imported json file.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { upVote, downVote } from '../store/actions/voteAction';
class Voter extends Component {
render() {
const { count, upVote, downVote, id} = this.props
return (
<div>
<button onClick={() => upVote(id)}>+</button>
The count is {count[id]}
<button onClick={() => downVote(id)}>-</button>
</div>
)
}
}
const mapDispatchToProps = dispatch => ({
upVote: (payload) => dispatch(upVote(payload)),
downVote: (payload) => dispatch(downVote(payload))
});
const mapStateToProps = (state) => ({
count: state.vote.count
})
export default connect(mapStateToProps, mapDispatchToProps)(Voter);
I think my issue comes with how i pass and update the payload in my reducer.
import {UP_VOTE,DOWN_VOTE} from '../actions/actionTypes'
import Mice from './../../imports/mice'
const initialState = {
count: new Array(Mice.length).fill(0)
}
const voteReducer = (state=initialState, action) => {
const id = action.payload
switch(action.type){
case UP_VOTE:
return{
...state, count: state.count[id] + 1
}
case DOWN_VOTE:
return{
...state, count: state.count[id] - 1
}
default:
return state
}
}
export default voteReducer;
I update the array, but every index is still changing and it appears i am still mutating the count array instead of an index inside it.
I have uploaded all my code to CodeSandbox for viewing and experimenting:
CodeSandbox Link
Thanks for reading
Use map method to create a new array, add change one element. The Redux switch will be:
switch (action.type) {
case UP_VOTE:
return {
...state,
count: state.count.map((vote, i) => (i === id ? vote + 1 : vote))
};
case DOWN_VOTE:
return {
...state,
count: state.count.map((vote, i) => (i === id ? vote - 1 : vote))
};
default:
return state;
}
Working code here https://codesandbox.io/s/74pmomo42j
I've taken two courses, treehouse and one on udemy, on react/redux and just when I think to myself "hey you got this, let's do a little practice" I run into some huge bug I can't seem to diagnose.
What I'm trying to do here sounds very simple, and in plain javascript it works. My state is an empty object state = {} and when my action is called, it creates an array inside of state noteName. So at the end of the day state should look like state = { noteName: [ ...state.noteName, action.payload]}.
When I console.log(this.props.inputvalue) it will return whatever is in the input element. I thought I understood objects because that consolelog should return the array noteName and not the actual value, correct?
Code
actions/index.js
export const INPUT_VALUE = 'INPUT_VALUE';
export function addNoteAction(text) {
return {
type: INPUT_VALUE,
payload: text
}
}
reducers/reducer_inputvalue.js
import { INPUT_VALUE } from '../actions';
// state is initialized as an empty object here
export default function(state = {}, action) {
switch (action.type) {
case INPUT_VALUE:
state.noteName = [];
// this SHOULD create an array that concats action.payload with
// whatever is already inside of state.name
return state.noteName = [...state.noteName, action.payload];
default:
return state;
}
}
noteitems.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class NoteItems extends Component {
render() {
return (
<ul>
{
this.props.inputvalue.noteName
?
this.props.inputvalue.noteName.map((note, index) => {
// this should iterate through noteName but returns undefined
return <li key={index}>{note}</li>;
})
:
<li>Nothing here</li>
}
</ul>
);
}
}
function mapStateToProps(state) {
return {
inputvalue: state.inputvalue
}
}
export default connect(mapStateToProps)(NoteItems);
This is happening because every time the action INPUT_VALUE is dispatched, you are resetting noteName. The main principle of redux is to not modify the state, but creating a new one based on the current. In your case:
const initialState = {
noteName: []
};
export default function(state = initialState, action) {
switch (action.type) {
case INPUT_VALUE: return {
noteName: [...state.noteName, action.payload]
};
default: return state;
}
}
You are overwriting state.noteName in the first line of your switch case.
switch (action.type) {
case INPUT_VALUE:
state.noteName = [];
In Redux, the point is to never overwrite a value, but to return a new value that might be a brand-new value, might be a value that is based on the old value (but still a new value... not overwriting the old), or it might be returning the old value (completely unmodified).
const counterReducer = (counter, action) => {
const options = {
[COUNTER_INCREMENT]: (counter, action) =>
({ value: counter.value + 1 }),
[COUNTER_DECREMENT]: (counter, action) =>
({ value: counter.value - 1 }),
[COUNTER_RESET]: (counter, action) =>
({ value: 0 })
};
const strategy = options[action.type];
return strategy ? strategy(counter, action) : counter;
};
At no point in that example am I modifying a value on counter. Everything is treated as read-only.