I am using the Switch component from MUI library. I want to achieve where only one Switch can be checked at a time. For instance, if Switch A is checked, when Switch B is checked, Switch A will be unchecked and vice versa. I am using the reducer pattern to achieve this.
Here is my code:
My reducer file:
// reducer.js
actionTypes = {
toggle_price: 'toggle_price'
}
export const initialState = {
fastprice: false,
spotprice: true,
}
export const toggleReducer = (state, action) => {
switch (action.type) {
case actionTypes.toggle_price: {
return {
...state,
[action.event.target.name]: action.event.target.checked
}
}
}
}
export const useToggler = ({ reducer = toggleReducer } = {}) => {
const [{ fastprice, spotprice}, dispatch] = useReducer(reducer, initialState)
const togglePrice = (event) => dispatch({ type: 'toggle_price', event })
return { togglePrice, fastprice, spotprice }
}
My component
//Component.js
const { togglePrice, spotprice, fastprice } = useToggler()
<FormControlLabel
control={<Switch onChange={e => toggleButton(e)} checked={fastprice} name='fastprice' />}label='Switch A' />
<FormControlLabel
control={<Switch onChange={e => toggleButton(e)} checked={spotprice} name='spotprice' />}label='Switch B' />
This code checks both Switchcomponents.
you can hold the two radio buttons (or more) as a group to control the state of them.
for example combine the two radio button into one object and let just on radio button to be different.
try this solution.
const { togglePrice, radioGroup } = useToggler();
return (
<div className="App">
<FormControlLabel
control={
<Switch
onChange={(e) => togglePrice(e)}
checked={radioGroup.fastprice}
name="fastprice"
/>
}
label="Switch A"
/>
<FormControlLabel
control={
<Switch
onChange={(e) => togglePrice(e)}
checked={radioGroup.spotprice}
name="spotprice"
/>
}
label="Switch B"
/>
</div>
);
}
const actionTypes = {
toggle_price: "toggle_price"
};
export const initialState = {
radioGroup: {
fastprice: false,
spotprice: true
}
};
export const toggleReducer = (state, action) => {
switch (action.type) {
case actionTypes.toggle_price: {
const m = { ...state.radioGroup };
for (let key in m) {
m[key] = !action.event.target.checked;
}
return {
...state,
radioGroup: {
...m,
[action.event.target.name]: action.event.target.checked
}
};
}
default:
return state;
}
};
export const useToggler = ({ reducer = toggleReducer } = {}) => {
const [{ radioGroup }, dispatch] = useReducer(reducer, initialState);
const togglePrice = (event) => dispatch({ type: "toggle_price", event });
return { togglePrice, radioGroup };
};
live - DEMO
https://codesandbox.io/embed/elastic-star-8o6e6i?fontsize=14&hidenavigation=1&theme=dark
Related
I built an E-commerce site for pizza. It could be viewed here: https://chinomso1995.github.io/dodosPizza/.
I have created an orders page and On the orders page when you try to increment items, it increments in two's. I have gone through the reducer code I wrote and the context code and I seem to have written everything correctly. I can't pinpoint where the problem is coming from.
JSX code for my reducer
const Storage = (cartItems) => {
localStorage.setItem('cart', JSON.stringify(cartItems.length > 0 ? cartItems: []));
}
export const sumItems = cartItems => {
Storage(cartItems);
let itemCount = cartItems.reduce((total, product) => total + product.quantity, 0);
let total = cartItems.reduce((total, product) => total + product.price * product.quantity, 0).toFixed(2);
return { itemCount, total }
}
export const CartReducer = (state, action) => {
switch (action.type) {
case "ADD_ITEM":
if (!state.cartItems.find(item => item.id === action.payload.id)) {
state.cartItems.push({
...action.payload,
quantity: 1
})
}
return {
...state,
...sumItems(state.cartItems),
cartItems: [...state.cartItems]
}
case "REMOVE_ITEM":
return {
...state,
...sumItems(state.cartItems.filter(item => item.id !== action.payload.id)),
cartItems: [...state.cartItems.filter(item => item.id !== action.payload.id)]
}
case "INCREASE":
state.cartItems[state.cartItems.findIndex(item => item.id === action.payload.id)].quantity++
return {
...state,
...sumItems(state.cartItems),
cartItems: [...state.cartItems]
}
case "DECREASE":
state.cartItems[state.cartItems.findIndex(item => item.id === action.payload.id)].quantity--
return {
...state,
...sumItems(state.cartItems),
cartItems: [...state.cartItems]
}
case "CHECKOUT":
return {
cartItems: [],
checkout: true,
...sumItems([]),
}
case "CLEAR":
return {
cartItems: [],
...sumItems([]),
}
default:
return state
}
}
JSX code for the context
import React, { createContext, useReducer, use } from 'react';
import { CartReducer, sumItems } from './OrderReducer';
export const CartContext = createContext()
const storage = localStorage.getItem('cart') ? JSON.parse(localStorage.getItem('cart')) : [];
const initialState = { cartItems: storage, ...sumItems(storage), checkout: false };
const CartContextProvider = ({children}) => {
const [state, dispatch] = useReducer(CartReducer, initialState)
const increase = payload => {
dispatch({type: 'INCREASE', payload})
}
const decrease = payload => {
dispatch({type: 'DECREASE', payload})
}
const addProduct = payload => {
dispatch({type: 'ADD_ITEM', payload})
}
const removeProduct = payload => {
dispatch({type: 'REMOVE_ITEM', payload})
}
const clearCart = () => {
dispatch({type: 'CLEAR'})
}
const handleCheckout = () => {
console.log('CHECKOUT', state);
dispatch({type: 'CHECKOUT'})
}
const contextValues = {
removeProduct,
addProduct,
increase,
decrease,
clearCart,
handleCheckout,
...state
}
return (
<CartContext.Provider value={contextValues} >
{ children }
</CartContext.Provider>
);
}
export default CartContextProvider;
JSX code for the Product Context
export const ProductsContext = createContext()
const ProductsContextProvider = ({children}) => {
const [pizzaproducts] = useState(Pizza)
const [sideproducts] = useState(Sides);
const [dessertproducts] = useState(Desserts)
const [drinkproducts] = useState(Drinks)
return (
<ProductsContext.Provider value={{sideproducts, dessertproducts, drinkproducts, pizzaproducts}} >
{ children }
</ProductsContext.Provider>
);
}
export default ProductsContextProvider;
JSX code for a section
const Sides = ()=> {
const {sideproducts} = useContext(ProductsContext);
const {addProduct, cartItems, increase} = useContext(CartContext);
const isInCart = sideproducts => {
return !!cartItems.find(item => item.id === sideproducts.id);
}
return(
<div className={styles.Sides} id='sides'>
<h1>Sides</h1>
<div className={styles.SidesContainer}>
{sideproducts.map(side=>{
return <div className={styles.SidesCard}>
<div className={styles.ImageContainer}>
<img src={side.image} alt="sausagerollone"/>
</div>
<div className={styles.SidesHeader}>
<div>
<h1>{side.name}</h1>
<p>{side.details}</p>
</div>
<div className={styles.SidesFooter}>
<h3>₦{side.price}</h3>
{
isInCart(side) &&
<button
onClick={() => increase(side)}>Add more</button>
}
{ !isInCart(side) &&
<button onClick={()=>addProduct(side)}>
<span>from ₦{side.price}</span>
<span>Add to basket</span>
</button>}
</div>
</div>
</div> })}
</div>
</div>
)
}
export default Sides;
In the increased case of the reducer code, I increment each item by one but it does increment by two in the app itself. This also affects the total price.
i have such problem: I'm making To-Do-List, and now I want to make EditMode for my tasks. But when I try to do it, it returns string not an array, and that's why I have 3 errors (map,some,filter = is not a function). So I don't know how to change state(task) and return changed array.
Some details: I'm using connect to get props.
Component's code
class Item extends React.Component {
state = {
statusChange: false,
task: ''
}
activeStatusChange = () => {
this.setState( {
statusChange: true
}
);
}
deActivateStatusChange = () => {
this.setState( {
statusChange: false
}
);
this.props.editTask(this.state.task)
}
onStatusChange = (e) => {
this.setState({
task: e.currentTarget.value
})
}
render(){
return (
<div className={s.item}>
<span onClick={this.props.editStatus} className={s.statusTask}>
{this.props.status ? <img src="https://img.icons8.com/doodle/48/000000/checkmark.png"/>
: <img src="https://img.icons8.com/emoji/48/000000/red-circle-emoji.png"/>}
</span>
{ this.state.statusChange
? <input onChange={this.onStatusChange} autoFocus={true} onBlur={this.deActivateStatusChange} value={this.state.task} />
: <span className={this.props.status === true ? s.task : s.taskFalse} onClick={this.activeStatusChange}> {this.props.task} </span>}
<span onClick={this.props.deleteTask} className={s.close}><img src="https://img.icons8.com/color/48/000000/close-window.png"/></span>
</div>
)
}
}
export default Item;
Reducer's code
import React from 'react'
import shortid from 'shortid';
const ADD_TASK = 'ADD_TASK'
const EDIT_STATUS = 'EDIT_STATUS'
const TASK_DELETE = 'TASK_DELETE'
const REMOVE_ALL_DONE = 'REMOVE_ALL_DONE'
const REMOVE_ALL_TASKS = 'REMOVE_ALL_TASKS'
const EDIT_TASK = 'EDIT_TASK'
const initialState = {
tasks: []
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TASK: {
return {
...state,
tasks: [{
id: shortid.generate(),
task: action.task,
status: false
}, ...state.tasks]
}
}
case EDIT_STATUS: {
return {
...state,
tasks: state.tasks.map(task => task.id === action.id ? {...task, status: !task.status} : task)
}
}
case TASK_DELETE: {
return {
...state,
tasks: state.tasks.filter(t => t.id !== action.id)
}
}
case REMOVE_ALL_DONE: {
return {
...state,
tasks: state.tasks.filter(t => !t.status)
}
}
case REMOVE_ALL_TASKS: {
return {
...state,
tasks: []
}
}
case EDIT_TASK: {
return {
...state,
tasks: action.task
}
}
default:
return state
}
}
export const addTask = task => ({type: 'ADD_TASK', task});
export const editStatus = id => ({type: 'EDIT_STATUS', id})
export const deleteTask = id => ({type: 'TASK_DELETE', id})
export const removeAllDone = () => ({type:'REMOVE_ALL_DONE'})
export const removeAllTasks = () => ({type: 'REMOVE_ALL_TASKS'})
export const editTask = task => ({type: 'EDIT_TASK', task})
export default mainReducer;
You should create a container that uses the methods mapDispatchToProps so you can use your actions in the component.
https://react-redux.js.org/using-react-redux/connect-mapdispatch
So lets do it. Just create a file that will be your container for that component and put the code like this:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Item from 'wherever your component is';
import { addTask } from 'wherever your action is';
const mapStateToProps = ({ }) => ({
// Here you can pass the redu state to your component
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
// Here you pass the action to your component
addTask
}, dispatch)
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Item);
Then when you want to use the Item component import it from the container and it will receive in props both the action and the state that you are passing from the container file.
In you Item component you can use the action like this:
// ITem component
render() {
return (
<button onClick={this.props.addTask} />
)
}
If any doubt just let me know!
This is just a sample code I am trying to control my controlled inputs using Redux, I add the Redux to my React project and add my reducer and action but everything works well except updating my component in one of my actions.
the following code is my Reducer:
import actionTypes from "./actions";
const uniqid = require("uniqid");
const firstID = uniqid();
const initialState = {
cons: [
{
value: "",
id: firstID,
added: false
}
],
pros: [
{
value: "",
id: firstID,
added: false
}
],
num: 0
};
const reducer = (state = initialState, action) => {
const newState = { ...state };
switch (action.type) {
case actionTypes.HANDLEINPUTCHANGE:
// const newState = state;
const changingItem = newState[action.case].find(item => {
return item.id === action.id;
});
const changingItemIndex = newState[action.case].findIndex(item => {
return item.id === action.id;
});
changingItem.value = action.event;
if (
changingItemIndex === newState[action.case].length - 1 &&
!changingItem.added
) {
alert(123);
const newItem = {
id: uniqid(),
value: "",
added: false
};
newState[action.case].push(newItem);
changingItem.added = true;
console.log(newState);
}
newState[action.case][changingItemIndex] = changingItem;
return newState;
case actionTypes.CLICK:
newState.num += 1;
return {
...newState
};
default:
return state;
}
};
export default reducer;
and the following code is my component, unfortunately, the HANDLEINPUTCHANGE action type did not update my component:
import React, { Component } from "react";
import FormElement from "../../base/components/formElement/FormElement";
import actionTypes from "../../base/store/actions";
import { connect } from "react-redux";
import "./style.scss";
class FormGenerator extends Component {
render() {
console.log(this.props);
return (
<ul className="row formGeneratorContainer fdiColumn">
<li onClick={this.props.click}>{this.props.num}</li>
{this.props[this.props.case].map((item, index) => {
return (
<li className="row formGeneratorItem" key={index}>
<div className="bullet d_flex jcCenter aiCenter">1</div>
{/* <FormElement onChange={(e,index,type,)}/> */}
<input
name={item.id}
type="text"
onChange={event =>
this.props.onFieldValueChange(
event.target.value,
index,
this.props.case,
item.id
)
}
/>
</li>
);
})}
</ul>
);
}
}
const mapStateToProps = state => {
return {
cons: state.cons,
pros: state.pros,
num: state.num
};
};
const mapDispachToProps = dispatch => {
return {
onFieldValueChange: (event, index, c, id) =>
dispatch({
event: event,
index: index,
case: c,
id: id,
type: actionTypes.HANDLEINPUTCHANGE
}),
click: () => dispatch({ type: actionTypes.CLICK })
};
};
export default connect(
mapStateToProps,
mapDispachToProps
)(FormGenerator);
You need to set value of your controlled component:
<input
name={item.id}
type="text"
value={item.value}
onChange={event =>
this.props.onFieldValueChange(
event.target.value,
index,
this.props.case,
item.id
)
}
/>
Other problems are in your reducer, you are mutating the redux state with these lines:
newState[action.case].push(newItem);
// ...
newState[action.case][changingItemIndex] = changingItem;
Look at these sections in the redux documentation:
Inserting and Removing Items in Arrays
Updating an Item in an Array
My react component gets API data from multiple axios gets. I need to map and filter the data in various ways and am not sure about whether I should be doing so in the react Containers or the redux store. Further, even though my data is mounted, I get undefined results often, perhaps due to the async nature of the actions.
The app is a collection builder for banknote collectors. Users can browse notes from worldwide, view, add and edit notes in collections (CollectionBuilder) and wantlists (WantListBuilder - seen below).
The data are arrays of objects in three different arrays (regions, countries, banknotes) sent from my own API.
regions = [{ region_name: "Africa"}, { region_name: "Asia" }, { region_name: "Caribbean"}, etc.] length = 9
countries = [{ name: "Angola", reg_id: 1 }, { name: "Argentina", reg_id: 8 }, { name: "Armenia", reg_id: 5}, etc.] length = 209
banknotes = [ { ctry_id: 1, catalog_no: "151A", denomination: 5, currency: "Kwanzas", issue_date: "2012", img_url: "https://yadda-yadda-yadda" }, etc] length = 282
The key component is comprised of a select where choosing a region (e.g. Africa) populates a second select of countries from that region. When a country is select, the notes from that country are rendered.
I have been able to use map and filter to populate the selects and render all notes, but I cannot filter the notes to remove items already in a collection and want list in the collection and wantlist builders.
Also, I try to keep my state clean, so I have focused on manipulating the props in my components instead. Is this a sensible approach.
**Select.js component**
* IMPORTS *
class Select extends Component {
componentDidMount() {
this.props.onGetRegions();
this.props.onGetCountries();
}
regionHandler = event => {
const selectedRegion = event.target.value;
this.props.onRegionChange(selectedRegion);
};
countryHandler = event => {
const selectedCountry = event.target.value;
this.props.onCountryChange(selectedCountry);
};
render() {
const { selectedRegion } = this.props;
const { selectedCountry } = this.props;
const region = this.props.regions.map(region => {
return (
<RegionOption
key={region.id}
id={region.id}
region={region.region_name}
/>
);
});
const filteredCountries = this.props.countries.filter(
country => country.reg_id === parseInt(selectedRegion)
);
const renderedCountries = filteredCountries.map(country => {
return (
<CountryOption
key={country.id}
id={country.id}
country={country.name}
/>
);
});
let label;
if (selectedRegion) {
label = <SelectLabelDirective />;
} else {
label = <SelectLabel />;
}
return (
<Container>
<Row>
<Col className={styles.Col}>
<label>Select a Region</label>
<hr />
<select
className={styles.Select}
name="regions"
value={selectedRegion}
onChange={this.regionHandler}
>
{region}
</select>
</Col>
<Col className={styles.Col}>
{label}
<hr />
<select
className={styles.Select}
name="countries"
value={selectedCountry}
onChange={this.countryHandler}
>
{renderedCountries}
</select>
</Col>
</Row>
</Container>
);
}
}
const mapStateToProps = state => {
return {
regions: state.select.regions,
countries: state.select.countries,
selectedRegion: state.select.selectedRegion,
selectedCountry: state.select.selectedCountry
};
};
const mapDispatchToProps = dispatch => {
return {
onGetRegions: () => dispatch(actions.getRegions()),
onGetCountries: () => dispatch(actions.getCountries()),
onRegionChange: selectedRegion =>
dispatch(actions.regionSelected(selectedRegion)),
onCountryChange: selectedCountry =>
dispatch(actions.countrySelected(selectedCountry))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Select);
**WantListBuilder.js container**
* IMPORTS *
class WantListBuilder extends Component {
state = {
fetched: false
};
componentDidMount() {
this.props.onFetchBanknotes();
this.props.onFetchWantlist();
}
render() {
let builder = <Spinner />;
if (this.props.banknotes) {
// const mapWant = this.props.wantlist.map(listnote => listnote)
// const filterNotes = this.props.banknotes.filter(banknote => banknote.banknote_id !== mapWant.note_id)
// console.log(mapWant[0], filterNotes);
builder = (
<Container>
<h1>WantListBuilder Builder</h1>
<Select />
<Banknotes
banknotes={this.props.banknotes}
region={this.props.selectedRegion}
country={this.props.selectedCountry}
wantlist={this.props.wantlist}
/>
</Container>
);
}
return <div>{builder}</div>;
}
}
const mapStateToProps = state => {
console.log("WANTLIST", state);
return {
banknotes: state.note.banknotes,
selectedRegion: state.select.selectedRegion,
selectedCountry: state.select.selectedCountry,
wantlist: state.wantlist.wantlist
};
};
const mapDispatchToProps = dispatch => {
return {
onFetchBanknotes: () => dispatch(actions.fetchBanknotes()),
onFetchWantlist: () => dispatch(wantListActions.fetchWantlist())
};
};
export default connect(mapStateToProps, mapDispatchToProps(WantListBuilder);
**wantListReducer.js (banknoteReducer.js is identical, but with banknote: [] in state)**
import * as actionTypes from "../../actions/wantlistActions/wantlistActionTypes";
const intitialState = {
wantlist: [],
fetching: false,
fetched: false,
error: false
};
const fetchWantlistStart = (state, action) => {
return { ...state, fetching: true }
};
const fetchWantlistFail = (state, action) => {
return {state, fetching: false, error: action.payload}
};
const fetchWantlist = (state, action) => {
return {
...state,
fetching: false,
fetched: true,
wantlist: action.payload
}
};
const reducer = (state = intitialState, action) => {
switch (action.type) {
case actionTypes.FETCH_WANTLIST_START:
return fetchWantlistStart(state, action);
case actionTypes.FETCH_WANTLIST_FAIL:
return fetchWantlistFail(state, action);
case actionTypes.FETCH_WANTLIST:
return fetchWantlist(state, action);
default:
return state;
}
};
export default reducer;
In the WantListBuilder, I want to send a filtered array to the Banknotes.js component (which renders a card with the notes image, country, denomination, etc.) The array will remove the notes that are already on the user's wantlist from the notes rendered on the select actions. I cannot seem to render the necessary array to send a props. Sorry for the overkill. It's my first question attempt.
In my project I'm using tcomb-form-native library to validation a form. Redux working fine but I can't pass value of inputs to reducer. I have to do this, because I want to create array with data from fields.
How can I pass values of my inputs to reducer?
Or maybe it's not possible with this library and I have to use another one?
Form.js
const mapDispatchToProps = dispatch => {
return {
closeExpenseDialog: (value) => dispatch({type: 'CLOSE_EXPENSE_DIALOG'}),
};
};
const mapStateToProps = state => {
return {
value: state.closeExpenseDialog.value,
};
};
const Form = t.form.Form;
const Expense = t.struct({
expense: t.String,
cost: t.Number
});
const options = {
fields: {
expense: {
error: 'This field is required'
},
cost: {
error: 'This field is required'
}
}
};
handleClick = () => {
const value = this._form.getValue();
if (value) {
console.log(value);
this.props.closeExpenseDialog(value);
} else {
console.log('validation failed');
}
}
<Form
type={Expense}
ref={c => this.props._form = c}
options={options}
value={this.props.value}
/>
<ActionButton
onPress={this.props.closeExpenseDialog}
title={title}
/>
Reducer.js
const initialState = {
value: {}
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case 'CLOSE_EXPENSE_DIALOG':
console.log('it works')
console.log(state.value) //undefined
default:
return state;
}
};
I needed add onChange() attribute and use it to pass object value to the Reducer.
Form.js
const Expense = t.struct({
expense: t.String,
cost: t.Number
});
const options = {
fields: {
expense: {
error: 'This field is required'
},
cost: {
error: 'This field is required'
}
}
};
handleClick = () => {
const value = this._form.getValue();
if (value) {
this.props.submitExpenseDialog();
}
}
<Form
type={Expense}
ref={c => this._form = c}
options={options}
value={this.props.value}
onChange={this.props.changeExpenseInputs}
/>
<ActionButton
onPress={this.handleClick}
title={title}
/>
Reducer.js
const initialState = {
value: {}
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case 'SUBMIT_EXPENSE_DIALOG':
console.log(state.value)
default:
return state;
}
};