I am using react/redux to build my app. There are multiple buttons and the app needs to keep track of states of whether these buttons were clicked. These buttons are all using the same reducer logic, which is to alternate the state from true to false whenever the button is clicked. The only differentiator is the name of the state. Is there a way to reduce my code by defining a "global" reducer that can be applied to multiple states? Please refer to this image for an example
Thank you!
This is how I would do it. Instead of dispatching unique action for every button type like MENU_BTN, SEARCH_BTN, etc.. I would dispatch { type: 'TOGGLE_BUTTON', payload: 'menu' } and in reducer
case TOGGLE_BUTTON:
return {
[payload]: !state[payload]
...state
}
You can then toggle buttons like this
const toggleButton = key => ({
type: 'TOGGLE_BUTTON',
payload: key
})
dispatch(toggleButton('menu'))
dispatch(toggleButton('search'))
That way you can keep track of as many buttons as you want. Your state will then look something like this
...
buttons: {
menu: true,
search: false
}
...
and you can easily write selector for every button
// Component displaying state of menu button //
let MyComponent = ({ menuButtonState }) => (
<div>
Menu button is {menuButtonState ? 'on' : 'off'}
</div>
)
// Helper function for creating selectors //
const createGetIsButtonToggledSelector = key => state => !!state.buttons[key]
// Selector getting state of a menu button from store //
const getIsMenuButtonToggled = createGetIsButtonToggledSelector('menu')
// Connecting MyComponent to menu button state //
MyComponent = connect(state => ({
menuButtonState: getIsMenuButtonToggled(state)
})(MyComponent)
You can create a generic higher-order reducer that accepts both a given reducer function and a name or identifier.
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
function createNamedWrapperReducer(reducerFunction, reducerName) {
return (state, action) => {
const { name } = action
const isInitializationCall = state === undefined
if (name !== reducerName && !isInitializationCall) return state
return reducerFunction(state, action)
}
}
const rootReducer = combineReducers({
counterA: createNamedWrapperReducer(counter, 'A'),
counterB: createNamedWrapperReducer(counter, 'B'),
counterC: createNamedWrapperReducer(counter, 'C')
})
The above createdNamedWrapperReducer() method should work out of the box.
See Redux's recipes for Reusing Reducer Logic for a more detailed explanation or more examples.
Related
I have a very simple redux boilerplate code that works fine if I use action?.type and not when I use action.type. My understanding is that the action is never null right? So, any idea why I'm getting the error:
Cannot read properties of undefined (reading 'type')
The way I've created the project is I have an action export const NAME_UPDATED = 'profile/updateName'; and an action creator for this action i.e. export function updateProfile(text) { return { type: NAME_UPDATED, payload: text }}.
My reducer is:
import { NAME_UPDATED } from './actions';
//initial state
const initialState = { name: 'Initial Name' }
//reducers
export function myReducer(state = initialState, action) {
switch (action?.type) {
case NAME_UPDATED: return { ...state, name: action.payload };
default: return state;
}
}
I'm dispatching the actions on a button press:
// Imports
const myDispatch = useDispatch();
const mySelector = useSelector(myReducer);
// Other code
<p>
The Current value of <code>name</code> in the store is: <code>{mySelector.name}</code>.
</p>
<button onClick={() => myDispatch(updateProfile('Updated Name'))}>
Learn React
</button>
// Other code
Here is the code sand box for more info: https://codesandbox.io/embed/redux-basic-example-ftrvs0?autoresize=1&fontsize=14&hidenavigation=1&theme=dark
useSelector allows you to extract data from the Redux store state, using a selector function. https://react-redux.js.org/api/hooks#useselector
In your case:
const mySelector = useSelector((store) => store)
I haven't been in React for a while and now I am revising. Well I faced error and tried debugging it for about 2hours and couldn't find bug. Well, the main logic of program goes like this:
There is one main context with cart object.
Main property is cart array where I store all products
If I add product with same name (I don't compare it with id's right now because it is small project for revising) it should just sum up old amount of that product with new amount
Well, I did all logic for adding but the problem started when I found out that for some reason when I continue adding products, it linearly doubles it up. I will leave github link here if you want to check full aplication. Also, there I will leave only important components. Maybe there is small mistake which I forget to consider. Also I removed logic for summing up amount of same products because that's not neccesary right now. Pushing into state array is important.
Github: https://github.com/AndNijaz/practice-react-
//Context
import React, { useEffect, useReducer, useState } from "react";
const CartContext = React.createContext({
cart: [],
totalAmount: 0,
totalPrice: 0,
addToCart: () => {},
setTotalAmount: () => {},
setTotalPrice: () => {},
});
const cartAction = (state, action) => {
const foodObject = action.value;
const arr = [];
console.log(state.foodArr);
if (action.type === "ADD_TO_CART") {
arr.push(foodObject);
state.foodArr = [...state.foodArr, ...arr];
return { ...state };
}
return { ...state };
};
export const CartContextProvider = (props) => {
const [cartState, setCartState] = useReducer(cartAction, {
foodArr: [],
totalAmount: 0,
totalPrice: 0,
});
const addToCart = (foodObj) => {
setCartState({ type: "ADD_TO_CART", value: foodObj });
};
return (
<CartContext.Provider
value={{
cart: cartState.foodArr,
totalAmount: cartState.totalAmount,
totalPrice: cartState.totalAmount,
addToCart: addToCart,
}}
>
{props.children}
</CartContext.Provider>
);
};
export default CartContext;
//Food.js
import React, { useContext, useState, useRef, useEffect } from "react";
import CartContext from "../../context/cart-context";
import Button from "../ui/Button";
import style from "./Food.module.css";
const Food = (props) => {
const ctx = useContext(CartContext);
const foodObj = props.value;
const amountInput = useRef();
const onClickHandler = () => {
const obj = {
name: foodObj.name,
description: foodObj.description,
price: foodObj.price,
value: +amountInput.current.value,
};
console.log(obj);
ctx.addToCart(obj);
};
return (
<div className={style["food"]}>
<div className={style["food__info"]}>
<p>{foodObj.name}</p>
<p>{foodObj.description}</p>
<p>{foodObj.price}$</p>
</div>
<div className={style["food__form"]}>
<div className={style["food__form-row"]}>
<p>Amount</p>
<input type="number" min="0" ref={amountInput} />
</div>
<Button type="button" onClick={onClickHandler}>
+Add
</Button>
</div>
</div>
);
};
export default Food;
//Button
import style from "./Button.module.css";
const Button = (props) => {
return (
<button
type={props.type}
className={style["button"]}
onClick={props.onClick}
>
{props.children}
</button>
);
};
export default Button;
Issue
The React.StrictMode component is exposing an unintentional side-effect.
See Detecting Unexpected Side Effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- here
The function passed to useReducer is double invoked.
const cartAction = (state, action) => {
const foodObject = action.value;
const arr = [];
console.log(state.foodArr);
if (action.type === "ADD_TO_CART") {
arr.push(foodObject); // <-- mutates arr array, pushes duplicates!
state.foodArr = [...state.foodArr, ...arr]; // <-- duplicates copied
return { ...state };
}
return { ...state };
};
Solution
Reducer functions are to be considered pure functions, taking the current state and an action and compute the next state. In the sense of pure functionality, the same next state should result from the same current state and action. The solution is only add the new foodObject object once, based on the current state.
Note also for the default "case" just return the current state object. Shallow copying the state without changing any data will unnecessarily trigger rerenders.
I suggest also renaming the reducer function to cartReducer so its purpose is more clear to future readers of your code.
const cartReducer = (state, action) => {
switch(action.type) {
case "ADD_TO_CART":
const foodObject = action.value;
return {
...state, // shallow copy current state into new state object
foodArr: [
...state.foodArr, // shallow copy current food array
foodObject, // append new food object
],
};
default:
return state;
}
};
...
useReducer(cartReducer, initialState);
Additional Suggestions
When adding an item to the cart, first check if the cart already contains that item, and if so, shallow copy the cart and the matching item and update the item's value property which appears to be the quantity.
Cart/item totals are generally computed values from existing state. As such these are considered derived state and they don't belong in state, these should computed when rendering. See Identify the minimal (but complete) representation of UI state. They can be memoized in the cart context if necessary.
I have a React Native scenario in which I have two components, one is a textInput and the other is a SVG drawing. By filling out the textInput and pressing a button, the text is inserted as a data instance to an array stored in a redux state using a dispatch action (let's call it textListState).
//TEXT INPUT COMPONENT
//this code is just for presentation. it will cause errors obviously
const dispatch = useDispatch();
const submitHandler = (enteredText) => {
dispatch(submitData(enteredText))
};
return(
<TextInput ...... />
<TouchableOpacity onPress={() => submitHandler(enteredText) } />
)
Now, the SVG component updates all the features' fill color stored in the textListState using a function (e.g. setFillColor). This is done inside a useEffect hook with a dependency set to a prop (e.g. propA).
Now the problem is that I need to add textListState as a dependency to the useEffect because I want the newly entered text from the textInput to be included in the SVG component. But by doing so, I am creating an infinite loop, because the setFillColor also updates the textListState.
//SVG COMPONENT
const [textList, setTextList] = useState([]);
const textListState = useSelector(state => state.textListState);
const dispatch = useDispatch();
const setFillColor= useCallback((id, fill) => {
dispatch(updateFillColor(id, fill))
}, [dispatch]);
useEffect(() => {
//INFINITE LOOP BECAUSE textListState KEEPS UPDATING WITH setFillColor
const curTextList = [];
for (const [i, v] of textListState.entries()) {
setFillColor(5, "white")
curTextList.push(
<someSVGComponent />
)
}
setTextList(curTextList);
}, [props.propA, textListState])
return(
<G>
{textList.map(x => x)}
</G>
)
How can I achieve to be able to add the newly inserted text to the SVG component without creating an infinite loop?
EDIT:
The redux action
export const UPDATE_TEXT = "UPDATE_TEXT";
export const SUBMIT_DATA = "SUBMIT_DATA";
export const updateFillColor = (id, fill) => {
return { type: UPDATE_TEXT, id: id, fill: fill }
};
export const submitData= (text) => {
return { type: SUBMIT_DATA, text: text }
};
The redux reducer
import { TEXT } from "../../data/texts";
import { UPDATE_TEXT } from "../actions/userActions";
import { INSERT_TEXT } from "../actions/userActions";
// Initial state when app launches
const initState = {
textListState: TEXT
};
const textReducer = (state = initState, action) => {
switch (action.type) {
case INSERT_TEXT :
return {
...state,
textListState: [
...state.textListState,
action.text
]
}
case UPDATE_TEXT :
const curText = state.textListState.filter(text => text.id === action.id)[0];
return {
...state,
textListState: [
...state.textListState.slice(0, curIndex), //everything before current text
curText.fill = action.fill,
...state.textListState.slice(curIndex + 1), //everything after current text
]
}
default:
return state;
}
};
export default textReducer;
if you want to trigger useEffect only when new text is added then use textListState.length instead of textListState in dependency list.
If you want to do it both on update and insert (i.e. you want to fire even when text is updated in input) then use a boolean flag textUpdated in textListState and keep toggling it whenever there is a update or insert and use textListState.textUpdated in dependency list.
I'm having problems with a checkbox in my code in two areas, The first one, in my reducer, I want to see if the current state of the checkbox is "true" or "false" but I keep getting syntax errors on the if.
const initialState = {
viewCheckbox: false
}
export default (state = initialState, action) => {
switch (action.type){
case 'VIEW_CHECKBOX':
return {
...state
if (viewCheckbox == false) {
viewCheckbox: true
} else {
viewCheckbox: false
}
}
default:
return: state
}
}
My second problem is with the mapDispatchToProps, I'm using a table to create multiple checkboxes and I want to be able to differentiate each one of them by ID, and when I do it like this, it checks every checkbox on the table.
const mapDispatchToProps = (dispatch) => ({
handleViewCheckbox: id => ev => {
dispatch(viewCheckboxSubmit(id, ev.target.checked))
}
})
And when I create the checkbox I do it like this:
<FormControlLabel
control={
<Checkbox
checked={checkedView}
onChange={handleViewCheckbox(n.id,checkedView)}
/>
}
label='See'
/>
You can't do an if inside an object like that. You can use the spread operator as you have done, add solo variables in which the key matches the variable you want to use, or you can add keys directly. try something like this:
case 'VIEW_CHECKBOX':
return {
...state,
viewCheckbox: !viewCheckbox
}
They're all going to toggle on and off because you're using the same bit of state to manage whether or not it is checked. If you want to go about it this way, you could try storing whether each one is checked in an object and just keep updating that.
{
checkboxid1: false,
checkboxid2: false,
checkboxid3: true // checked
}
then the checked prop for your checkbox component would look like..
checked={this.props.isChecked[n.id]}
assuming you pulled that object out in mapStateToProps and called it isChecked
I am creating this UI using react and redux. Below is the image of the UI.
This is image of UI
Now it currently displays the data when 'Categories' is clicked. Then when I click on unmapped link ,it shows unmapped products of 'Categories'. But when I click on 'Addons' and then on unmapped link ,it still shows the unmapped products of 'Categories'. How do I write the reducer so that it shows the unmapped products of 'Addons' as well. In my react main file I use (this.props.menu_items) that's why it shows the data of 'Categories' when clicked unmapped after Addons.
This is my reducer file.
import { AppConstants } from '../constants';
const initialState = {
state: [],
menu_items: [],
menu_items_copy: [],
addon_items: [],
unmapped: false,
};
export default (state = initialState, action) => {
switch (action.type) {
case AppConstants.getMenuItems:
return {
...state,
}
case AppConstants.getMenuItemsSuccess:
return {
...state,
menu_items: action.menu_items,//This contains the data showed when
// 'categories' is clicked
menu_items_copy: action.menu_items,
unmapped: false,
}
case AppConstants.getAddonsItems:
return {
...state,
}
case AppConstants.getAddonsItemsSuccess:
return {
...state,
menu_items: action.addon_items,//Here I update the data to addons data
}
case AppConstants.showAllProducts:
return {
...state,
menu_items: action.menu_items,
unmapped: false
}
case AppConstants.getUnmappedMenuItemsSuccess:
return {
...state,
unmapped: true,
menu_items: state.menu_items_copy.filter((item) => {-->How do I write
here for action.addon_items also
return (item.productList.length == 0)
})
}
case AppConstants.hideError:
return {
...state,
error: null
}
default:
return state
}
};
The only state I would change via reducer when unmapped is clicked would be to set unmapped: true / false.
There is no need to filter the items array and store it back into the state, as you already have all the info you need to derive your combined items at the point where you pass it to the view.
Have a read about derived state and selectors here http://redux.js.org/docs/recipes/ComputingDerivedData.html
In order to do this you would use a selector to combine the relevant parts of your state to produce a single list of items that is derived from both the items arrays, depending on the unmapped flag. The reselect library is a great helper to do this while memoising for performance, it will only recompute if any of the inputs change, otherwise returns the previously cached value
import {createSelector} from 'reselect'
const selectMenuItems = state => state.menu_items
const selectAddonItems = state => state.addon_items
const selectUnmapped = state => state.unmapped
const selectItemsToShow = createSelector(
selectMenuItems,
selectAddonItems,
selectUnmapped,
(menuItems, addonItems, unmapped) => {
// if unmapped is set - combine all items and filter the unmapped ones
if (unmapped) {
return menuItems.concat(addonItems).filter(item => !item.productList.length)
}
// else just return the menu items unfiltered
return menuItems
}
)
// this selector can then be used when passing data to your view as prop elsewhere
// or can be used in `connect` props mapping as in the linked example)
<ItemsList items={selectItemsToShow(state)} />