Language used : javascript with react / redux
My project : I have a multiple step form. At every step,when a user write something or check someting i'm using redux to store the state. I have one reducer but I'm creating an action for every step of the form
What i would like to do : I would like to have only one action to update the state step by step.
What i'm doing now (working fine) :
my page who contain each step
const Form = () => {
return (
<div className="page">
<form>
{
{
1: <StepOne />,
2: <StepTwo />,
3: <StepThree />,
}[buttonDatas.pageNumber]
}
</form>
</div>
);
};
export default Form;
here one example of a step component (stepOne)
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addName } from '../../../actions/form.action.js';
import { isEmpty } from '../../../middlewares/verification.js';
export const StepOne = () => {
const dispatch = useDispatch();
const usersList = useSelector((state) => state.userReducer);
const [userName, setUserName] = useState();
useEffect(() => {
dispatch(addName(userName));
}, [dispatch, userName]);
return (
<div>
<label>Select the user name</label>
<select
name="name"
onChange={(e) => {
const userSelected = e.target.value;
setUserName(userSelected);
}}
defaultValue={'default'}
>
<option value="default" hidden disabled>
Select a user
</option>
{!isEmpty(usersList[0]) &&
usersList.map((user) => {
return (
<option key={user.id}>
{user.fullName}
</option>
);
})}
</select>
</div>
);
};
here my reducer :
import {
ADD_NAME,
ADD_PHONE,
ADD_ADDRESS,
} from '../actions/form.action';
const initialState = {
userInfo: {
name: '',
phone : '',
},
address: ''
};
export default function formReducer(state = initialState, actions) {
switch (actions.type) {
case ADD_NAME:
state = {
...state,
userInfo: {
name: actions.payload,
},
};
return state;
case ADD_PHONE:
state = {
...state,
userInfo: {
phone: actions.payload,
},
};
return state;
case ADD_ADDRESS:
state = {
...state,
address: actions.payload,
},
};
return state;
default:
return state;
}
}
Is there a better way to write it ?
You can create one object that includes all the necessary property values throughout your multi-step form wizard layout and maintain only one action to save the data in the Redux store. Instead of making each action item for a single property of the identical form.
hereby am giving you a reference which will help you to organise your code based on your requirement.
I will recommend you to go through below two links:
Redux Form multi-step wizard form
Video Tutorial of creating a multi-step form using react Hooks.
With the indications of nimish, i've used redux toolkit and react-hook-form and it's working fine.
What i've change :
Create a slice file (for my reducer and action ) with redux toolkit
import { createSlice } from '#reduxjs/toolkit';
const formSlice = createSlice({
name: 'form',
initialState: {
userInfo: {
name: '',
phone: ''
},
address: ''
},
reducers: {
selectUserName: (state, action) => {
state.userInfo.name = action.payload;
},
addUserPhone: (state, action) => {
state.userInfo.phone = action.payload;
},
addAddress: (state, action) => {
state.address = action.payload;
},
},
});
export const reducer = formSlice.reducer;
export const {
selectUserName,
addUserPhone,
addAddress,
} = formSlice.actions;
use it in my userInfo component
import React from 'react';
import { userName } from './userName';
import { userPhone } from './userPhone';
import { useDispatch, useSelector } from 'react-redux';
import {
selectUserName,
addUserPhone,
} from '../../../reducer/form.slice';
import { useForm } from 'react-hook-form';
export const UserInfo = () => {
const dispatch = useDispatch();
const state = useSelector((state) => state.reducer);
const { register, handleSubmit } = useForm({
defaultValues: {},
});
const onChange = (data) => {
dispatch(selectUserName(data.name);
dispatch(addUserPhone(data.phone));
};
return (
<div>
<h2 className="title"> Step One : Information User</h2>
<form onChange={handleSubmit(onChange)} className="form">
<userName register={register} />
<userPhone register={register} />
</form>
//to see your result
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
);
};
in the child comp
import React from 'react';
import { useSelector } from 'react-redux';
import { isEmpty } from '../../../middlewares/verification.js';
export const UserName = ({ register }) => {
const userList = useSelector((state) => state.userReducer);
return (
<div className="form_group">
<label>Select the user name</label>
<select
className="select"
name="name"
{...register('userName')}
defaultValue={'default'}
>
<option value="default" hidden disabled>
Select a user
</option>
{!isEmpty(userList[0]) &&
userList.map((user) => {
return (
<option value={user.fullName} key={user.mail}>
{user.fullName}
</option>
);
})}
</select>
</div>
);
};
thank you.
Related
I build a MERN stack ecommerce using redux. In the part of cart components I add product to the cart and also to localStorage. When I refresh the page the items disappear from the page but it is still in localStorage and I can't find the problem.
This is my cart reducer code:
import { ADD_TO_CART } from "../constants/cartConstants";
export const cartReducer = (state = { cartItems: [] }, action) => {
switch (action.type) {
case ADD_TO_CART:
const item = action.payload;
const isItemExist = state.cartItems.find(
(i) => i.product === item.product
);
if (isItemExist) {
return {
...state,
cartItems: state.cartItems.forEach((i) =>
i.product === isItemExist.product ? item : i
),
};
} else {
return {
...state,
cartItems: [...state.cartItems, item],
};
}
default:
return state;
}
};
and this is my store initialState code:
const initialState = {
cart: {
cartItems: localStorage.getItem("cartItems")
? JSON.parse(localStorage.getItem("cartItems"))
: [],
},
};
const store = configureStore(
{ reducer },
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
cart.jsx
import React, { Fragment, useEffect, useState } from "react";
import "./Cart.css";
import { CartItems } from "../";
import { useDispatch, useSelector } from "react-redux";
import { addItemsToCart } from "../../actions/cartActions";
const Cart = () => {
const dispatch = useDispatch();
const { cartItems } = useSelector((state) => state.cart);
const increaseQuantity = (id, quantity, stock) => {
const newQty = quantity + 1;
if (stock < quantity) {
return;
}
dispatch(addItemsToCart(id, newQty));
};
return (
<Fragment>
<div className="cart__page">
<div className="cart__header">
<p>Product</p>
<p>Quantity</p>
<p>Subtotal</p>
</div>
{cartItems &&
cartItems?.map((item) => (
<div key={item?.product} className="cartContainer">
<CartItems item={item} />
<div className="cart__Input">
<button>+</button>
<input type="number" readOnly value={item?.quantity} />
<button>-</button>
</div>
<p className="Cart__subtotal">
{`$${item?.price * item?.quantity}`}
</p>
</div>
))
}
I'm trying to use useEffect hook but the data come by redux doesn't save in localStorage.
The configureStore function takes only a single configuration object that takes reducer, middleware, devTools, preloadedState, and enhancers properties.
See configureStore.
It appears you are correctly accessing the persisted state from localStorage, but then not passing the initial state correctly to the store configurator.
import { configureStore } from '#reduxjs/toolkit';
const initialState = {
cart: {
cartItems: JSON.parse(localStorage.getItem("cartItems")) ?? [],
},
};
const store = configureStore({
reducer,
preloadedState: initialState,
});
export default store;
If your redux state persistence needs change or grow then I'd suggest taking a look at redux-persist. If you are already familiar with Redux then this is about a 5-15 minute integration the first time.
help me please in Redux filter onInput change! I have two state: 1) items - is array (name number and tel number), 2) search (this state get value by input). When i enter name and phone number and click onSubmit in component will sent state "items". The next i see this contacts on the page. I have another input, his task, filter by contacts array. How can i filtered array state no change him and when filter value === '' i can see all items in state "items".
myStore
import { configureStore } from "#reduxjs/toolkit";
import { createSlice } from "#reduxjs/toolkit";
const contactsSlice = createSlice({
name: "contacts",
initialState: {
items: [],
search: "",
},
reducers: {
add(state, action) {
state.items.push(action.payload);
},
remove(state, action) {
state.items = state.items.filter((arrow) => arrow.id !== action.payload);
},
search(state, action) {
state.search = action.payload;
state.items.filter((item) =>
item.name.toLowerCase().includes(state.search)
);
},
},
});
export const { add, remove, search } = contactsSlice.actions;
export const store = configureStore({
reducer: {
contacts: contactsSlice.reducer,
},
});
My Component Filter
import { useSelector, useDispatch } from "react-redux";
import { ContainerSearch, Title, Input } from "./Filter.styled";
import { search } from "../Redux/store";
const Filter = () => {
const dispatch = useDispatch();
const onChange = (e) => {
dispatch(search(e.currentTarget.value.toLocaleLowerCase()));
};
return (
<ContainerSearch>
<Title>Find contacts by name</Title>
<Input type="text" onChange={onChange} placeholder="Search name" />
</ContainerSearch>
);
};
export default Filter;
and my List items
import { useSelector, useDispatch } from "react-redux";
import { remove } from "../Redux/store";
import {
ContainerList,
Title,
Wrapper,
Item,
ButtonClose,
TextList,
} from "./ContactList.styled";
export const List = () => {
const selector = useSelector((state) => state.contacts.items);
const dispatch = useDispatch();
const deleteContact = (contactId) => {
dispatch(remove(contactId));
};
return (
<ContainerList>
<Title>Contacts</Title>
{selector.length > 0 ? (
<Wrapper>
{selector.map(({ id, name, number }, index) => (
<Item key={id} index={index}>
{name} : {number}
<ButtonClose onClick={() => deleteContact(id)}>
✗
</ButtonClose>
</Item>
))}
</Wrapper>
) : (
<TextList>No Contacts</TextList>
)}
</ContainerList>
);
};
Do you mean it is working fine, but only wants to show everything in case no searchTerm is provided?
if so, then you can just change the filter logic to:
state.items.filter((item) => {
const searchTerm = (state.search || "").toLowerCase().trim();
return searchTerm
? (item.name || "").toLowerCase().includes(state.search)
: true;
});
I'm creating a simple book list application using react and managing the state using redux. I've only 2 components that deal with the state, the input component dispatches the action and payload, while the output component should get the updated state data. Using the useSelector() hook inside the output component, I can see that the state is updated, however, when I try to access the array of objects and a length property, I get 'cannot read properties of undefined'. Please let me know what did I miss here.
Here is my code for Input Component
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import styles from "./Input.module.css";
const Input = () => {
const dispatch = useDispatch();
const [bookObject, setBookObject] = useState({
bookName: "",
authorName: "",
});
const inputChangeHandler = (e) => {
if (e.target.id === "bookName" || e.target.id === "authorName") {
setBookObject({
bookName: document.getElementById("bookName").value,
authorName: document.getElementById("authorName").value,
});
}
};
const submitHandler = (e) => {
if (bookObject.bookName !== "" && bookObject.authorName !== "") {
dispatch({
type: "GET_INPUT",
payload: {
name: bookObject.bookName,
author: bookObject.authorName,
id: Math.random(),
},
});
setBookObject({ bookName: "", authorName: "" });
} else {
alert("Enter valid Details");
}
e.preventDefault();
};
return (
<form className={styles.form} onSubmit={submitHandler}>
<div>
<label>Book's Name</label>
<input
id="bookName"
type="text"
placeholder="Enter the book's name"
onChange={inputChangeHandler}
value={bookObject.bookName}
/>
</div>
<div>
<label>Author's Name</label>
<input
id="authorName"
type="text"
placeholder="Enter the Author's name"
onChange={inputChangeHandler}
value={bookObject.authorName}
/>
</div>
<button>Submit</button>
</form>
);
};
export default Input;
Here is the code of the Output Component
import React, { Fragment } from "react";
import styles from "./Output.module.css";
import { useSelector } from "react-redux";
const Output = () => {
const outputState = useSelector((state) => state);
const length = outputState.length
const outputObj = outputState.outputObj
return (
<Fragment>
{length !== undefined ? (
<div className={styles.div}>
<h4>Book List</h4>
<ul>
{outputObj.map((book) => (
<li key={book.id}>
{book.name}, written by {book.author}
</li>
))}
</ul>
</div>
) : (
<h4>The Book List is empty</h4>
)}
</Fragment>
);
};
When I console log the outputState, I get a proper object with outputObj array and a length property, but when I try to access outputState.outputObj or outputState.length, I get the error mentioned. I've also tried using two useSelectors each to get the data separately, but in vain.
Here is the code of reducer
import { createStore } from "redux";
const defaultState = {
outputObj: [],
length: undefined,
};
const bookListReducer = (state = defaultState, action) => {
if (action.type === "GET_INPUT") {
state.outputObj = [...state.outputObj, action.payload];
return {
outputObj: state.outputObj,
length: state.outputObj.length,
};
}
};
const store = createStore(bookListReducer);
export default store;
If there was no action, or your action's type was not "GET_INPUT" your reducer will return undefined, therefore the state will be flushed. Update your code as follows.
const bookListReducer = (state = defaultState, action) => {
if (action.type === "GET_INPUT") {
state.outputObj = [...state.outputObj, action.payload];
return {
outputObj: state.outputObj,
length: state.outputObj.length,
};
}
return state; // <- HERE
};
I have a React app, the state is managed with Redux.
The user can search for a game and a multitude of results, whose titles loosely match the query, will appear on submitting. Every time the user enters another query, the previous results are replaced by the new ones.
After 5-6 searches, the app slows down considerably. After the 7th search, it stops working entirely, Chrome throwing a 'page not responding' notice.
The redux slice looks like this:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import rawg from '../../apis/rawg';
const initialState = {
results: [],
};
export const fetchGames = createAsyncThunk(
'gamesSearch/fetchGames',
async (query) => {
const response = await rawg.get('/games', {
params: {
search: query,
},
});
return response.data.results;
}
);
const gamesSearchSlice = createSlice({
name: 'gamesSearch',
initialState,
reducers: {},
extraReducers: {
[fetchGames.fulfilled]: (state, action) => {
const results = action.payload;
const parsedResults = results.map((result) => {
return {
name: result.name,
slug: result.slug,
backgroundImage: result.background_image,
genres: result.genres.map((genre) => genre.name).join(', '),
id: result.id,
released: result.released
? `${result.released.split('-')[2]}.${
result.released.split('-')[1]
}.${result.released.split('-')[0]}`
: null,
};
});
},
},
});
export default gamesSearchSlice.reducer;
export const selectResults = (state) => state.gamesSearch.results;
And the component from which the fetch is dispatched looks like so:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchGames } from './gamesSearchSlice';
const SearchBar = () => {
const [query, setQuery] = useState('');
const dispatch = useDispatch();
const onSubmit = (e) => {
e.preventDefault();
if (!query) return;
dispatch(fetchGames(query));
};
return (
<div className="searchbar">
<form onSubmit={onSubmit}>
<input
className="searchbar__input"
type="text"
placeholder="Search for a game..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
</div>
);
};
export default SearchBar;
Am I missing some detail about how React and Redux work together, or is it something wrong with my code from a fundamentals perspective (meaning: I am not handling data efficiently enough with JavaScript)?
So, i wrote a test project to explore react, react-router and react-redux.
After i got everything working fine i laid my eyes again on Settings.jsx and i am wondering how could i make it less verbose and error prone:
import React, { Component } from "react";
import { connect } from "react-redux";
class Settings extends Component {
state = { name: this.props.settings.name };
render() {
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<button onClick={e => this.props.changeName(this.state.name)}>
Change
</button>
</div>
);
}
}
const mapState = state => ({ settings: state.settings });
const mapDispatch = dispatch => {
return {
changeName(name) {
dispatch({ type: "setName", name });
}
};
};
export default connect(
mapState,
mapDispatch
)(Settings);
My first idea was to convert it into a functional component, but it's said that they don't have state and i need the state to locally handle the input.
With #babel/plugin-proposal-decorators, connect can be used as a decorator:
import React, { Component } from "react";
import { connect } from "react-redux";
const mapState = state => ({ settings: state.settings });
const mapDispatch = dispatch => {
return {
changeName(name) {
dispatch({ type: "setName", name });
}
};
};
#connect(mapState, mapDispatch)
export default class Settings extends Component {
state = { name: this.props.settings.name };
render() {
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<button onClick={e => this.props.changeName(this.state.name)}>
Change
</button>
</div>
);
}
}
small, but imho nice simplification
also, you could use concise syntax with your mapDispatch:
const mapDispatch = dispatch => ({
changeName(name) {
dispatch({ type: "setName", name });
}
});
you can do this if you want to to add the typing text in store:
Settings.js
import React from "react";
import { changeName, typingName } from '../actions/settingsActions'
import { connect } from "react-redux";
const Settings = () => {
const { changeName, typingName, typedName, submittedName } = this.props
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={typedName}
onChange={e => typingName(e.target.value)}/>
<button onClick={changeName(submittedName)}>
Change
</button>
</div>
);
}
const mapState = state => ({
typedName: state.typedName,
submittedName: state.submittedName
});
const mapDispatchToProps = dispatch => ({
typingName: x => dispatch(typingName(x)),
changeName: x => dispatch(changeName(x))
})
export default connect(
mapState,
mapDispatch
)(Settings);
settingsActions.js
export const typingName = payload => ({
type: 'TYPING_NAME',
payload
});
export const changeName = payload => ({
type: 'CHANGE_NAME',
payload
});
settingsReducer.js
export const typingName = (state = [], action) => {
switch (action.type) {
case 'TYPING_NAME':
return [...state, action.payload];
default:
return state;
}
};
export const changeName = (state = '', action) => {
switch (action.type) {
case 'CHANGING_NAME':
return action.payload;
default:
return state;
}
};
You could maybe achieve something like this. But validating the typing state inside the component then sending the final result to the store as you did is a better idea I think, to avoid so much verbose.
Also you should of course create a constants file, but I guess you know already.