What is the best way to update item in React Redux - reactjs

I'm learning React Redux and i'm writing todo-list app. The last missing piece is option to editting selected item (onClick). I have problem with doing that.
Could you tell me how i can achieve that?
I looking for solution like that:
Select item which you want to update (by clicking on edit icon)
Form should change into "editing mode" so user can pass new value into editing item.
When value is passed, then user click on submit button.
I don't know how to make editing mode so my input form can handle updating existing item.
// ACTION
export const editTask = (task) => ({
type: "EDIT_TASK",
payload: task,
});
// REDUCER
const INITIAL_STATE = {
tasks: [],
alert: {
show: false,
message: "",
type: "",
},
isEditing: false,
};
const reducer = (state, action) => {
switch (action.type) {
case "EDIT_TASK":
return {
...state,
isEditing: true,
// take current tasks and updatedTask
tasks: [...state.tasks, action.payload],
alert: setAlert(true, "Task has changed succesfull 🌈", "success"),
};
default:
return state;
On my todo.jsx
const TasksTodo = () => {
const [inputValue, setInputValue] = useState("");
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
const editTaskFromList = (item) => {
// match tasks from list with that selected
const taskToEdit = state.tasks.find(
(taskToFind) => taskToFind.id === item.id
);
const editedTask = {
taskToEdit,
title: inputValue,
};
dispatch(editTask([...state.tasks, editedTask]));
};
const handleSubmit = (e) => {
e.preventDefault();
if (inputValue) {
const newItem = {
id: new Date().getTime().toString(),
title: inputValue,
};
dispatch(addNewTask(newItem));
setInputValue("");
} else {
dispatch({ type: "NO_VALUE" });
}
};
const handleChange = (e) => {
setInputValue(e.target.value);
};
return (
<div className="tasks-list-section">
<h3 className="tasks__title">tasks List </h3>
{state.alert.show && (
<Alert
{...state.alert}
removeAlert={() => dispatch(changeAlertState())}
/>
)}
<form onSubmit={handleSubmit} className="tasks__form">
<input
type="text"
value={inputValue}
placeholder="e.g homework"
className="tasks__input"
onChange={handleChange}
/>
<button type="submit" className="tasks__submit-btn">
{state.isEditing ? "edit" : "submit"}
</button>
</form>
{state.tasks.length > 0 && (
<div className="tasks__container">
<Tasks
listItems={state.tasks}
removeTask={removeTaskFromList}
editTask={editTaskFromList}
/>
</div>
)}
</div>
);
};
TaskList Component:
const Tasks = ({ listItems, editTask, removeTask }) => {
return (
<div className="tasks-list">
{listItems.map((item) => {
const { id, title } = item;
return (
<div key={id} className="tasks__item">
<p className="tasks__item--title">{title}</p>
<div className="tasks__button-group">
<button
type="button"
className="tasks__button-group--edit"
onClick={() => editTask(item)}
>
<RiEditBoxLine className="size" />
</button>
<button
type="button"
className="tasks__button-group--delete"
onClick={() => removeTask(item)}
>
<RiDeleteBin6Line className="size" />
</button>
</div>
</div>
);
})}
</div>
);
};

Please find the simple workaround for this.
Page.js ( Main Component)
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addTask, editTask, removeTask } from "./actions";
const TaskList = ({ tasks, removeTask, editTask }) => {
return (
<div>
{tasks.map((item) => (
<div key={item.id}>
<p>{item.title}</p>
<button type="button" onClick={(e) => editTask(item)}>
Edit
</button>
<button type="button" onClick={(e) => removeTask(item)}>
Remove
</button>
</div>
))}
</div>
);
};
function Page() {
const tasks = useSelector((state) => state.tasks);
const dispatch = useDispatch();
const [inputValue, setInputValue] = useState("");
const [editingTaskIndex, setEditingTaskIndex] = useState(null);
const editTaskFromList = (task) => {
const taskToEdit = tasks.find((item) => item.id === task.id);
const taskIndex = tasks.indexOf(taskToEdit);
setEditingTaskIndex(taskIndex);
setInputValue(taskToEdit.title);
};
const removeTaskFromList = (task) => {
const taskToDelete = tasks.find((item) => item.id === task.id);
const taskIndex = tasks.indexOf(taskToDelete);
dispatch(removeTask(taskIndex));
};
const handleSubmit = (e) => {
e.preventDefault();
if (editingTaskIndex === null) {
const newItem = {
id: new Date().getTime().toString(),
title: inputValue
};
dispatch(addTask(newItem));
} else {
const editingItem = tasks[editingTaskIndex];
editingItem.title = inputValue;
dispatch(editTask(editingTaskIndex, editingItem));
}
setInputValue("");
setEditingTaskIndex(null);
};
const handleChange = (e) => {
setInputValue(e.target.value);
};
return (
<div>
<h3>Task List</h3>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
placeholder="e.g: homework"
onChange={handleChange}
/>
<button>Submit</button>
</form>
<TaskList
tasks={tasks}
editTask={editTaskFromList}
removeTask={removeTaskFromList}
/>
</div>
);
}
export default Page;
reducer.js
const initialState = {
tasks: [],
isEditing: false
};
function taskReducer(state = initialState, action) {
switch (action.type) {
case "ADD_TASK":
return { ...state, tasks: [...state.tasks, action.payload] };
case "EDIT_TASK":
console.log(action.payload);
const currentTasks = Array.from(state.tasks);
currentTasks[action.payload.taskIndex] = action.payload.task;
return { ...state, tasks: currentTasks };
case "REMOVE_TASK":
const allTasks = Array.from(state.tasks);
allTasks.splice(action.payload, 1);
return {
...state,
tasks: allTasks
};
default:
return state;
}
}
export default taskReducer;
Checkout the sandbox link

I found different solution. Rate my code if you can.
On my reducer
case "EDIT_TASK":
return {
...state,
isEditing: true,
tasks: [
...state.tasks.filter((task) => task.id !== action.payload.id),
action.payload,
],
alert: setAlert(true, "Task has changed succesfull 🌈", "success"),
};
tasks.jsx
const editTaskFromList = (item) => {
const taskToEdit = state.tasks.find(
(taskToFind) => taskToFind.id === item.id
);
setEditingTaskID(taskToEdit.id);
setInputValue(taskToEdit.title);
};
const handleSubmit = (e) => {
e.preventDefault();
if (editingTaskID === null) {
if (inputValue) {
const newItem = {
id: new Date().getTime().toString(),
title: inputValue,
};
dispatch(addNewTask(newItem));
setInputValue("");
} else {
alert("Value cannot be empty!");
}
} else {
const editingItem = state.tasks.find((task) => task.id === editingTaskID);
const newItem = { ...editingItem, title: inputValue };
dispatch(editTask(newItem));
setInputValue("");
setEditingTaskID(null);
}
};

Related

How can i test add filters button when clicked it opens search filters popup but before the button appears there is loading spinner?

add filter button is appeared when loading spinner is completed(means there is dispatch trigger action called loadListings which set loading to true).
I'm using react testing library, how can i achieve this by using mockedAxios? or any idea.
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
Search.js
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useNavigate} from '#reach/router';
import Picture from '#/components/picture';
import Button from '#/components/button';
import Pagination from '#/components/pagination';
import Tag from '#/components/tag';
import Spinner from '#/components/basic-spinner';
import Footer from '#/components/footer/footer';
import {
getAppliedSelectFilters,
getListings,
getListingsTotal,
listingsLoading,
} from '#/selectors/search';
import {showPopup} from '#/redux/modules/app/actions';
import {
saveDraftFilters,
clearDraftFilters,
draftSelectFilter,
clearAppliedFilter,
clearAllAppliedFilters,
loadListings,
} from '#/redux/modules/search/actions';
import {getSelectFilters, getSelectFiltersGroups} from '#/selectors/search';
import SearchFilterPopup from '#/components/search-filter-popup';
const Search = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const appliedFilters = useSelector(getAppliedSelectFilters);
const hits = useSelector(getListings);
const hitsCount = useSelector(getListingsTotal);
const loading = useSelector(listingsLoading);
const hitsPerPage = 10;
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
const showFilters = () => {
dispatch(
showPopup({
content: (
<SearchFilterPopup
saveDraftFilters={saveDraftFilters}
clearDraftFilters={clearDraftFilters}
draftSelectFilter={draftSelectFilter}
loadListings={loadListings}
selectFiltersSelector={getSelectFilters}
selectFiltersGroupsSelector={getSelectFiltersGroups}
/>
),
showClose: false,
}),
);
};
const handleTagClicked = item => {
dispatch(clearAppliedFilter(item));
dispatch(loadListings());
};
// eslint-disable-next-line #typescript-eslint/no-unused-vars
const handleClearAllFilters = () => {
dispatch(clearAllAppliedFilters());
dispatch(loadListings());
};
const handleBusinessSelected = ({identity, urlName}) => () => {
navigate(`/listings/${urlName}/${identity}`);
};
const handlePaginationPageClick = data => {
const indexOfLastHit = (data.selected + 1) * hitsPerPage;
const pages = indexOfLastHit - hitsPerPage;
dispatch(loadListings(pages));
};
return (
<section className='np-flex np-w-full np-filter-container np-overflow-x-hidden np-max-w-1300px np-flex-col np-justify-between np-mx-auto np-min-h-screen'>
<div className='np-flex np-pd np-flex-col np-justify-center'>
{!loading ? (
<div className='np-w-full'>
<div className='np-w-full'>
<h3 className='np-font-primary np-result-places np-mb-3 np-pt-8 np-truncate'>
<span>{hitsCount === 0 ? hitsCount : `${hitsCount}+`}</span>{' '}
{`Place${hitsCount !== 1 ? 's' : ''}`} in Dar es Salaam
</h3>
<div className='np-mb-3 np-flex'>
<Button
variant='link'
className='np-font-semibold np-uppercase'
onClick={showFilters}
>
<span>Add Filters</span>
</Button>
</div>
<div className='np-overflow-x-auto np-flex np-w-full'>
{appliedFilters.map(item => (
<Tag
key={item.name}
item={item}
labelKey='label'
toBe='removed'
onClick={handleTagClicked}
className='np-Tag-width'
/>
))}
{appliedFilters.length ? (
<Button variant='link' onClick={handleClearAllFilters}>
Clear all filters
</Button>
) : null}
</div>
</div>
</div>
) : null}
<div className='np-flex np-flex-wrap np-justify-center'>
{loading ? (
<div className='np-pt-32 np-h-80vh np-w-full np-flex np-justify-center'>
<Spinner size='large' label='loading' color='primary' />
</div>
) : hitsCount > 0 ? (
hits.map(hit => (
<div
key={hit.id}
className='np-search-card'
onClick={handleBusinessSelected(hit)}
>
<Picture
height='2/3'
src={`${
process.env.IMAGE_SERVICE_URL
}/cover:entropy/340x226/${hit.photos.length > 0 &&
hit.photos[hit.photos.length - 1].name}`}
className='np-overflow-hidden np-rounded-sm np-cursor-pointer'
/>
<section className='np-py-2 np-leading-normal'>
<h4 className='np-truncate'>{hit.category}</h4>
<h3 className='np-font-primary np-font-medium np-cursor-pointer np-truncate np-capitalize'>
{hit.name}{' '}
</h3>
<h4 className='np-text np-text-gray np-truncate'>
<span className='icon location'></span> {hit.location}
</h4>
</section>
</div>
))
) : null}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
actions.js
export const clearDraftFilters = () => ({
type: types.CLEAR_DRAFT_FILTERS,
});
export const loadListings = payload => ({
type: types.LOAD_LISTINGS,
payload,
});
export const loadListingStart = () => ({
type: types.LOAD_LISTINGS_START,
});
export const loadListingSuccess = payload => ({
type: types.LOAD_LISTINGS_SUCCESS,
payload,
});
export const loadListingError = error => ({
type: types.LOAD_LISTINGS_ERROR,
payload: error,
error: true,
});
epics.js
export const loadListingsEpic = (action$, state$, {API}) =>
action$.pipe(
filter(action => action.type === types.LOAD_LISTINGS),
switchMap(action => {
const features = getAppliedPlaceFeatureFilters(state$.value).join(',');
const category = getAppliedPlaceCategoryFilters(state$.value).join(',');
const q = get(state$.value, 'search.q', '');
const page = action.payload ? action.payload : null;
const query = omitEmpty({q, category, features, page, limit: 12});
return defer(() => API.getListings(query)).pipe(
switchMap(response => {
const [error, result] = response;
if (error) {
return of(actions.loadListingError(error));
}
return of(actions.loadListingSuccess(result));
}),
startWith(actions.loadListingStart()),
);
}),
);
reducers.js
const initialState = {
selectFilters: [],
suggestions: {docs: [], total: 0, loading: false},
};
export default (state = initialState, action) => {
switch (action.type) {
case types.LOAD_LISTINGS_START:
return {
...state,
listings: {loading: true},
};
case types.LOAD_LISTINGS_SUCCESS:
return {
...state,
listings: {loading: false, ...action.payload},
};
case types.LOAD_LISTINGS_ERROR:
return {
...state,
listings: {loading: false, error: action.payload},
};
default:
return state;
}
};
Search.test.js
import React from 'react';
import {render, screen, fireEvent} from '#/utils/testUtils';
import {waitFor} from '#testing-library/react';
import mockedAxios from 'axios';
import Search from './Search';
import { async } from 'rxjs/internal/scheduler/async';
jest.mock('axios');
describe('Search', () => {
afterEach(() => {
jest.resetAllMocks();
});
test('should render search result filters popup when add filters button is clicked', async () =>{
render(<Search />);
await waitFor(() => fireEvent.click(screen.getByRole('button', {name: 'Add Filters'}))
)
})
});

I have a question about an issue where checkboxes are automatically checked when dropping a file into a drag and drop zone

When selecting files and dropping them into the drag-and-drop zone
I implemented it so that the file row list is output.
There is a problem.
The check box is automatically checked.
In developer tools, the confirmChecked function is called as many as the number of file rows.
Could this be related?
Why is the onChecked function automatically executed when drag and drop?
If anyone knows the cause of this problem or how to fix it, please let me know.
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import '../styles/FileAttach2.scss';
function FileAttach2(props) {
// state
// 체크된 항목
const [indexesArrayForChekedList, setIndexesArrayForChekedList] = useState([]);
const [filesToUpload, setFilesToUpload] = useState([]);
// 1122
const moveRowUp = params => {
};
const moveRowDown = params => {
};
// checkbox 관련
const confirmChecked = useCallback(
index => {
if (indexesArrayForChekedList.includes(index)) {
return false;
} else {
return true;
}
},
[indexesArrayForChekedList],
);
const handleCheckedStatus = useCallback(
(e, idx) => {
const checked = e.target.checked;
if (checked) {
const indexArrayForUpdate = indexesArrayForChekedList.filter(el => {
return el != idx;
});
setIndexesArrayForChekedList(indexArrayForUpdate);
} else {
setIndexesArrayForChekedList(prev => [...prev, idx]);
}
},
[indexesArrayForChekedList],
);
const deleteRowByIndex = useCallback(
idx => {
const afterDeleteRow = filesToUpload.filter(file => {
return file.index != idx;
});
setFilesToUpload(afterDeleteRow);
},
[filesToUpload],
);
// dropzone 관련
const onDrop = acceptedFiles => {
const filesData = acceptedFiles.map((file, index) => {
return { index: filesToUpload.length + index, name: file.name, size: file.size };
});
setFilesToUpload(prev => [...prev, ...filesData]);
};
const { getRootProps, getInputProps, open, acceptedFiles } = useDropzone({
noClick: true,
noKeyboard: true,
onDrop,
});
// FileRow template
const files = filesToUpload.map(file => (
<div className="fileRow" key={file.index}>
<div>
<input
type="checkBox"
onChange={e => handleCheckedStatus(e, file.index)}
checked={confirmChecked(file.index)}
/>
</div>
<div>{file.name}</div>
<div>{file.size} (bytes)</div>
<button type="button" onClick={() => deleteRowByIndex(file.index)}>
삭제
</button>
</div>
));
return (
<div className="">
<button type="button" onClick={open}>
Open File Dialog
</button>
<div {...getRootProps({ className: 'dropzone' })}>
<input {...getInputProps()} />
{filesToUpload.length !== 0 ? (
<div>
<div className="fileRowHeader">
<button onClick={moveRowDown}>아래로</button>
<button onClick={moveRowUp}>위로</button>
</div>
<div>{files}</div>
</div>
) : (
''
)}
<div></div>
</div>
</div>
);
}
export default FileAttach2;
You are returning true when indexesArrayForChekedList includes your index which will always be true at the beginning.
Try below code in your confirmChecked function
const confirmChecked = useCallback(
index => indexesArrayForChekedList.includes(index),
[indexesArrayForChekedList],
);

TypeError: Cannot read property 'length' of undefined in react js

My code is supposed to generate a popup of a product then add the product to the wishlist page. It gives an error that length is not defined, I think it because I misused hooks, I tried adding const [listVariations, setListVariations] = useState(0) in place of const [listVariations, setListVariations] = useState([]) but it still gives out same error can someone help me sort it out thanks in advance!
import React, { Fragment, useContext, useState, useEffect} from 'react'
import './Sass/Index.scss'
import { WishListContext } from '../../contexts/WishListContext'
import { ProductContext } from '../../contexts/ProductContext'
import Popup from "reactjs-popup";
const Product = ({product, id, productIndex}) => {
const{name, price, image, variation, option, unit, total} = product
const {wishListState, wishListDispatch} = useContext(WishListContext)
const {productState} = useContext(ProductContext)
const [popupVariation, setPopupVariation] = useState(false)
const [popupOption, setPopupOption] = useState(false)
const [listVariations, setListVariations] = useState([])
const [listOptions, setListOptions] = useState([])
const [selectedOption, setSelectedOption] = useState({})
const [updateToggle, setUpdateToggle] = useState(false)
useEffect(()=> {
generatePopupVariation()
generatePopupOptions()
}, [productState])
const updateLocalStorage = (products) => {
if (products.length > 0) {
wishListDispatch({
type:"REPLACE_WISH",
payload: products.map(item => item)
})
}
}
const generatePopupOptions = () => {
const products = productState.products
if (products.length > 0) {
const finedProduct = products.find(item => item.id === id)
const priceOptions = finedProduct?.data.priceOptions
setListOptions(priceOptions && priceOptions[0].options)
}
}
const generatePopupOptionContent= () => {
if ( listOptions.length > 0 ) {
const filtered = listOptions.filter(item => item.is_available !== 'No')
return filtered.map((item, index) => <li key={index}><span>{item.option}</span><span>Price: {item.price}</span><div className="btn btn-primary" onClick={() => changeOption(index, item.option, item.price)} >Select</div></li>)
}
}
const changeOption = (index, option, price) => {
const action = {
id,
option,
price
}
if (option) {
wishListDispatch({
type:'UPDATE_OPTION_WISH',
action
})
updateLocalStorage(wishListState.products)
modalOption('close')
}
}
const generatePopupVariation = () => {
const products = productState.products
if (products.length > 0) {
const finedProduct = products.find(item => item.id === id)
const priceOptions = finedProduct?.data.priceOptions
setListVariations(priceOptions)
}
}
const generatePopupVariationContent = () => {
if ( listVariations.length > 0 ) {
console.log(listVariations.length)
return listVariations.map((item, index) => <li className="btn btn-primary" onClick={() => changeVariation(index, item.variation, item.options)} key={index}>{item.variation}</li>)
}
}
const changeVariation = (index, variation, options) => {
const action = {
id,
variation
}
if (variation) {
wishListDispatch({
type:'UPDATE_VARIATION_WISH',
action
})
updateLocalStorage(wishListState.products)
modalVariation('close')
}
}
const handleRemove = () => {
if (wishListState) {
if( wishListState.products.length > 0) {
wishListDispatch({
type: 'REMOVE_WISH',
payload: productIndex
})
}
}
}
const handleUnit = (control) => {
if (wishListState) {
if( wishListState.products.length > 0) {
wishListDispatch({
type: 'ADD_UNIT_WISH',
payload: {index: productIndex, control:control}
})
}
}
}
const modalVariation = action => {
if (action === 'open') {
setPopupVariation(true)
} else if (action === 'close') {
setPopupVariation(false)
}
}
const modalOption = action => {
if (action === 'open') {
setPopupOption(true)
} else if (action === 'close') {
setPopupOption(false)
}
}
return (
<Fragment>
<Popup
open={popupVariation}
closeOnDocumentClick
onClose={()=> modalVariation('close')}
>
<div className="popup-content">
<ul>
{generatePopupVariationContent()}
</ul>
<span onClick={() => modalVariation('close')}>X</span>
</div>
</Popup>
<Popup
open={popupOption}
closeOnDocumentClick
onClose={()=> modalOption('close')}
>
<div className="popup-content">
<ul>
{generatePopupOptionContent()}
</ul>
<span onClick={() => modalOption('close')}>X</span>
</div>
</Popup>
<div className="card">
<div className="card-wrapper">
<div className="left">
<img className="card-img" src={ image } alt={ name } />
</div>
<div className="right">
<div className="card-remove" onClick={handleRemove}>
<i className="times" name="close"></i>
</div>
<h4 className="card-title">{ name }</h4>
{ variation && <p className="card-variation"><span>Variation:</span><span>{variation} <span onClick={()=> modalVariation('open')} className="change-variation"><ion-icon name="create"></ion-icon></span></span> </p>}
{ option && <p className="card-option"><span>Option:</span><span> { option } <span onClick={()=> modalOption('open')} className="change-option"><ion-icon name="create"></ion-icon></span></span></p>}
<div className="card-unit"><span>Qty:</span>
<div className="card-units-wrapper">
<i className="plus" name="add" onClick={() => handleUnit('+')}></i>
<div className="card-units">{unit}</div>
<i className="minus" name="remove" onClick={() => handleUnit('-')}></i>
</div>
</div>
<p className="card-price"><span>Price:</span><span> {price} </span></p>
<p className="card-total"><span>Total:</span> <span>{total} </span></p>
</div>
</div>
</div>
</Fragment>
)
}
export default Product
ProductContext.js
import React, { createContext, useReducer } from 'react'
import ProductReducer from '../reducers/ProductReducer'
export const ProductContext = createContext()
const initState = {
products: []
}
const ProductProvider = props => {
const [ productState, productDispatch ] = useReducer(ProductReducer, initState)
const value = {productState, productDispatch}
return (
<ProductContext.Provider value={value}>
{props.children}
</ProductContext.Provider>
)
}
export default ProductProvider
ProductReducer.js
const ProductReducer = (state, action) => {
switch (action.type) {
case "LOAD_PRODUCTS":
return {...state, products: [...action.payload]}
case "ADD_PRODUCT":
return {...state, products: [...state.products, action.payload]}
default:
return state
}
}
export default ProductReducer;
WishlistContext.js
import React, { createContext, useReducer } from 'react'
import WishListReducer from '../reducers/WishListReducer'
export const WishListContext = createContext()
const initState = {
products: [],
select_variation:null
}
const WishListProvider = props => {
const [ wishListState, wishListDispatch ] = useReducer(WishListReducer, initState)
const value = {wishListState, wishListDispatch }
return (
<WishListContext.Provider value={value}>
{props.children}
</WishListContext.Provider>
)
}
export default WishListProvider
WishlistReducer.js
const WishListReducer = (state, action) => {
switch (action.type) {
case "ADD_WISH":
return addWish(state, action)
case "REPLACE_WISH":
return replaceWish(state, action)
case "REMOVE_WISH":
return removeWish(state, action)
case "ADD_UNIT_WISH":
return unitWish(state, action)
case "UPDATE_VARIATION_WISH":
return updateVariation(state, action)
case "UPDATE_OPTION_WISH":
return updateOption(state, action)
default:
return state
}
}
const addWish = (state, action) => {
localStorage.setItem('wish-list', JSON.stringify({...state, products: [...state.products, action.payload] }))
return {...state, products: [...state.products, action.payload] }
}
const replaceWish = (state, action) => {
localStorage.setItem('wish-list', JSON.stringify({...state, products: [...action.payload] }))
return {...state, products: [...action.payload] }
}
const removeWish = (state, action) => {
const copyState = {...state }
copyState.products.splice(action.payload, 1);
localStorage.setItem('wish-list', JSON.stringify({...state, products: [...copyState.products] }))
return {...state, products: [...copyState.products] }
}
const unitWish = (state, action) => {
const copyState = {...state }
const index = copyState.products[action.payload.index]
const ctrl = action.payload.control
const unit = index.unit
const price = index.price
if (ctrl == "+") {
index.unit = Number(unit) + 1
index.total = Number(price) * (Number(unit) + 1)
}
if (ctrl == "-") {
if (Number(unit) <= 1) {
index.unit = 1
} else {
index.unit = Number(unit) - 1
index.total = Number(price) * (Number(unit) - 1)
}
}
localStorage.setItem('wish-list', JSON.stringify({...state, products: [...copyState.products] }))
return {...state, products: [...copyState.products] }
}
const updateVariation = (state, action) => {
const id = action.action.id
const variation = action.action.variation
const copyState = {...state }
const product = copyState.products.find(item => item.id == id)
product.variation = variation
return {...state, products: [...copyState.products] }
}
const updateOption = (state, action) => {
const id = action.action.id
const option = action.action.option
const price = action.action.price
const copyState = {...state }
const product = copyState.products.find(item => item.id === id)
product.option = option
product.price = price
product.total = product.unit * price
return {...state, products: [...copyState.products] }
}
export default WishListReducer;
Basically, some of the content used with .length is undefined. The best way to learn which one is undefined, is to use typeof. You can use typeof variableName to determine what is the type of the variable. If some of the types in the code are not List or Array, it will create an error. To debug, you can put alert("Type is not valid") combined with methods to see. (Btw we are not having those data and determine what is errored.)

optimistic ui updates - react

I imagine this is a basic in react but I'm not sure how to get it to work, basically when I delete, create or edit anything in my components I want the change to happen in realtime without refreshing the page, I've achieved it at some level with the search function but not entirely sure how to do with for the delete function for example:
Here is what I'm working with, how would I get this to work with my axios delete function?
Thanks
import { connect } from 'react-redux';
import { fetchTournaments } from '../actions/tournaments';
import Item from './Item';
import EditTournament from './EditTournament';
import axios from 'axios';
import '../styles/Item.css';
class SearchAndDisplay extends React.PureComponent {
componentDidMount() {
this.props.fetchTournaments();
}
state = {
searchCriteria: '',
isLoading: false
};
handleChange = event => {
this.setState({
searchCriteria: event.target.value
});
};
async handleDelete(id) {
const url = `http://localhost:4000/tournaments/`;
await axios
.delete(url + id)
.then(res => {
console.log(res.data);
})
.catch(err => {
console.log(err);
});
}
formatDate(date) {
let options = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
};
let newDate = new Date(Date.parse(date));
let format = new Intl.DateTimeFormat('default', options).format(newDate);
return format;
}
handleChange = event => {
this.setState({ searchCriteria: event.target.value });
};
renderList() {
let tournmentsArray = this.props.tournaments;
const filterTournaments = tournmentsArray.filter(item =>
item.name.includes(this.state.searchCriteria)
);
if (filterTournaments === undefined || filterTournaments.length === 0) {
return (
<React.Fragment>
<div className="notFound">
Something went wrong.
<br />
<button
className="notFoundButton"
onClick={() => {
this.setState({ searchCriteria: '' });
}}
>
Retry
</button>
</div>
</React.Fragment>
);
} else {
return filterTournaments.map(item => (
<Item
key={item.name}
name={item.name}
organizer={item.organizer}
participants={Object.values(item.participants)}
game={item.game}
start={this.formatDate(item.startDate)}
>
<div className="buttonBar">
<EditTournament id={item.id} />
<button
className="button"
onClick={() => {
if (
window.confirm('Are you sure you want to delete this item?')
) {
this.handleDelete(item.id);
}
}}
>
Delete
</button>
</div>
</Item>
));
}
}
render() {
return (
<div className="container">
<input
onChange={this.handleChange}
className="input"
placeholder="Search..."
id="searchField"
value={this.state.searchCriteria}
/>
<div className="row">{this.renderList()}</div>
</div>
);
}
}
function mapStateToProps({ tournaments }) {
return {
tournaments: Object.values(tournaments).flat()
};
}
export default connect(mapStateToProps, {
fetchTournaments
})(SearchAndDisplay);
unlike delete the create and edit data is handled by redux like so:
Create tournament:
import { reduxForm, Field } from 'redux-form';
import '../styles/promptForms.css';
import '../styles/Header.css';
import { connect } from 'react-redux';
import { createTournaments } from '../actions/tournaments';
class CreateTournamentPromptFrom extends React.Component {
constructor(props) {
super(props);
this.state = {
showHide: false
};
}
createTournamentButton() {
return (
<div>
<button
className="genericButton"
onClick={() => this.setState({ showHide: true })}
>
CREATE TOURNAMENT
</button>
</div>
);
}
renderInput = ({ input, label }) => {
return (
<div>
<label>{label}</label>
<br />
<input className="promptInput" {...input} autoComplete="off" />
</div>
);
};
onSubmit = formValues => {
this.props.createTournaments(formValues);
};
render() {
const { showHide } = this.state;
return (
<React.Fragment>
<div className={`overlay ${showHide ? 'toggle' : ''} `} />
<div className={`promptBox ${showHide ? 'toggle' : ''} `}>
<h3>localhost:3000 says</h3>
<form onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field
name="name"
component={this.renderInput}
label="Enter Tournament:"
/>
<button className="okayButton">OK</button>
</form>
<button
className="cancelButton"
onClick={() => this.setState({ showHide: false })}
>
Cancel
</button>
</div>
{this.createTournamentButton()}
</React.Fragment>
);
}
}
const formWrapped = reduxForm({
form: 'promptForm'
})(CreateTournamentPromptFrom);
export default connect(null, { createTournaments })(formWrapped);
actions:
import {
FETCH_TOURNAMENTS,
FETCH_TOURNAMENT,
CREATE_TOURNAMENT,
EDIT_TOURNAMENT
} from './types';
import { API_TOURNAMENTS_URL } from '../constants/api';
import axios from 'axios';
export const fetchTournaments = () => async dispatch => {
const response = await axios.get(API_TOURNAMENTS_URL);
dispatch({
type: FETCH_TOURNAMENTS,
payload: response.data.flat()
});
};
export const fetchTournament = id => async dispatch => {
const response = await axios.get(`http://localhost:4000/tournaments/${id}`);
dispatch({ type: FETCH_TOURNAMENT, payload: response.data });
};
export const createTournaments = formValues => async dispatch => {
const response = await axios.post(API_TOURNAMENTS_URL, {
...formValues
});
dispatch({ type: CREATE_TOURNAMENT, payload: response.data });
};
export const editTournaments = (id, formValues) => async dispatch => {
const response = await axios.patch(
`http://localhost:4000/tournaments/${id}`,
formValues
);
dispatch({ type: EDIT_TOURNAMENT, payload: response.data });
};
reducers:
import _ from 'lodash';
import {
FETCH_TOURNAMENT,
CREATE_TOURNAMENT,
FETCH_TOURNAMENTS,
EDIT_TOURNAMENT,
DELETE_TOURNAMENT
} from '../actions/types';
export default (state = {}, action) => {
switch (action.type) {
case FETCH_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
case FETCH_TOURNAMENTS:
return { ...state, [action.payload.id]: action.payload };
case CREATE_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
case EDIT_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
case DELETE_TOURNAMENT:
return _.omit(state, action.payload);
default:
return state;
}
};
To "optimistically" delete an item from state you'll need to immediately delete it from state to reflect the change right away in the UI. BUT you will need to add extra redux state to "hold" a pending delete with your backend. When the delete is successful you clear the held delete, if it fails you clear the held delete and add it back in to your regular data (and perhaps display some error message or toast, etc..).
I see you don't do the delete via redux, so use local component state and you'll have to filter your tournament data when rendering.
class SearchAndDisplay extends PureComponent {
componentDidMount() {
this.props.fetchTournaments();
}
state = {
searchCriteria: "",
isLoading: false,
optimisticTournaments: null // <-- state to hold temp "deleted" data
};
handleChange = event => {
this.setState({
searchCriteria: event.target.value
});
};
async handleDelete(id) {
console.log("delete id", id);
// optimistically remove element
this.setState({
optimisticTournaments: this.props.tournaments.filter(
item => item.id !== id
)
});
await axios
.delete(url + id)
.then(res => {
console.log(res.data);
// Need to create a call back to let parent know element was deleted
this.props.deleteSuccess(id);
})
.catch(err => {
console.log(err);
alert("Failed to delete");
})
.finally(() => {
this.setState({ optimisticTournaments: null });
});
}
formatDate(date) {
let options = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false
};
let newDate = new Date(Date.parse(date));
let format = new Intl.DateTimeFormat("default", options).format(newDate);
return format;
}
handleChange = event => {
this.setState({ searchCriteria: event.target.value });
};
renderList() {
let tournmentsArray =
this.state.optimisticTournaments || this.props.tournaments;
const filterTournaments = tournmentsArray.filter(item =>
item.name.includes(this.state.searchCriteria)
);
if (filterTournaments === undefined || filterTournaments.length === 0) {
return (
<React.Fragment>
<div className="notFound">
Something went wrong.
<br />
<button
className="notFoundButton"
onClick={() => {
this.setState({ searchCriteria: "" });
}}
>
Retry
</button>
</div>
</React.Fragment>
);
} else {
return filterTournaments.map(item => (
<Item
key={item.name}
name={item.name}
organizer={item.organizer}
participants={Object.values(item.participants)}
game={item.game}
start={this.formatDate(item.startDate)}
>
<div className="buttonBar">
<EditTournament id={item.id} />
<button
className="button"
onClick={() => {
if (
window.confirm("Are you sure you want to delete this item?")
) {
this.handleDelete(item.id);
}
}}
>
Delete
</button>
</div>
</Item>
));
}
}
render() {
return (
<div className="container">
<input
onChange={this.handleChange}
className="input"
placeholder="Search..."
id="searchField"
value={this.state.searchCriteria}
/>
<div className="row">{this.renderList()}</div>
</div>
);
}
}
Here is how you can do using forceUpdate() (Since you don't want to use state):
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component {
constructor() {
super();
this.items = [
{id: 1, name: "item 1"},
{id: 2, name: "item 2"},
];
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete(index) {
const newState = [...this.items];
delete newState[index];
// or
// newState.splice(index, 1);
this.items = newState;
this.forceUpdate();
}
render() {
return (
<div>
{
this.items.map((item, index) => {
return (
<div>
{item.name}
<button onClick={() => this.handleDelete(index)}>Delete item</button>
</div>
)
})
}
</div>
);
}
}
render(<App />, document.getElementById('root'));
Just pass the index in the map method :
...map((item, index) => ...);
Do it in the then() after your axios call.
Note that the documentation highly advice you to avoid using forceUpdate() when you can, so you really should use a state for this, I don't see any good reason for you not to use it here.
Here is a quick repro on Stackblitz.

Where does the reducer get the state from?

I am trying to understand redux with the help of an online todo application resource.
However, I can't seem to figure out where does the 'todos' reducer get the initial state from ?
I've consoled the state but can't seem to wrap my head around it ?
After the initial render, state is consoled 3 times as,
[ ]
[ ]
[ state object ]
Link: 'https://codepen.io/iamrkcheers/pen/rNNoBvB'
Any help is appreciated.
Thank You.
// --------- actions start ----------
const ADD_TODO = "ADD_TODO";
const TOGGLE_TODO = "TOGGLE_TODO";
const SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};
let nextTodoId = 3;
function addTodo(text) {
return {
type: ADD_TODO,
id: nextTodoId++,
text
}
}
function toggleTodo(id) {
return {
type: TOGGLE_TODO,
id
}
}
function setVisibilityFilter(filter) {
return {
type: SET_VISIBILITY_FILTER,
filter
}
}
// --------- actions end ----------
// --------- reducers start ----------
function todos(state = [], action) {
console.log('state is:',state);
switch (action.type) {
case ADD_TODO: {
return [...state, {
text: action.text,
completed: false,
id: action.id
}];
}
case TOGGLE_TODO: {
return state.map((todo, id) => {
if (id === action.id) {
return Object.assign({}, todo, {
completed: !todo.completed
});
}
return todo;
});
}
default: {
return state;
}
}
}
function visibilityFilter(state = VisibilityFilters.SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER: {
return action.filter;
}
default: {
return state
}
}
}
const todoApp = Redux.combineReducers({
visibilityFilter,
todos
});
// --------- reducers end ----------
// --------- components start ----------
const App = () => {
const getDate = date => new Date(date);
const days = ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"];
return (
<div className="block">
<div className="info-date">
<div className="date">{ getDate(Date.now()).toLocaleDateString("ru") }</div>
<div className="day">{ days[getDate(Date.now()).getDay()] }</div>
</div>
<AddTodo />
<Footer />
<VisibleTodoList />
</div>
);
};
const Footer = () => {
return (
<div className="filters">
<FilterLink filter="SHOW_ALL">Все задачи</FilterLink>
<FilterLink filter="SHOW_ACTIVE">Активные</FilterLink>
<FilterLink filter="SHOW_COMPLETED">Завершенные</FilterLink>
</div>
);
};
const Link = ({ active, children, onClick }) => {
if (active) {
return <span className="filter-item non-active">{ children }</span>
}
return (
<a className="filter-item" href="#" onClick = { event => {
event.preventDefault();
onClick();
} }>{ children }</a>
);
};
const Todo = ({ onClick, completed, text }) => {
const styles = {
textDecoration: completed ? "line-through" : "none"
};
return (
<li onClick = { onClick } style = { styles }>
<a>{ text }</a>
</li>
);
};
const TodoList = ({ todos, onTodoClick }) => {
return (
<div className="list">
<ul>
{
todos.map(todo => <Todo
key = { todo.id } { ...todo }
onClick = { () => onTodoClick(todo.id) } />)
}
</ul>
</div>
);
};
// --------- components end ----------
// --------- containers start ----------
let AddTodo = ({ dispatch }) => {
let input;
return (
<div>
<form className="addForm" onSubmit = { event => {
event.preventDefault();
if (!input.value.trim()) {
return;
}
dispatch(addTodo(input.value));
input.value = "";
} }>
<input type="text" placeholder="Что нужно сделать?" ref = { node => input = node }/>
<button type="submit" className="btn"></button>
</form>
</div>
);
};
AddTodo = ReactRedux.connect()(AddTodo);
var mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
};
};
var mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter));
}
};
};
const FilterLink = ReactRedux.connect(
mapStateToProps,
mapDispatchToProps
)(Link);
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case "SHOW_ALL": {
return todos;
}
case "SHOW_COMPLETED": {
return todos.filter(todo => todo.completed);
}
case "SHOW_ACTIVE": {
return todos.filter(todo => !todo.completed);
}
default: {
return todos;
}
}
};
var mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
};
var mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id));
}
};
};
const VisibleTodoList = ReactRedux.connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
// --------- containers end ----------
// --------- application start ----------
const initialState = {
visibilityFilter: "SHOW_ALL",
todos: [
{
id: 0,
text: "Изучить React",
completed: true
},
{
id: 1,
text: "Изучить Redux",
completed: true
},
{
id: 2,
text: "Написать приложение \"Список задач\"",
completed: false
}
]
};
let store = Redux.createStore(todoApp, initialState);
ReactDOM.render(
<ReactRedux.Provider store = { store }>
<App />
</ReactRedux.Provider>,
document.querySelector("#root")
);
// --------- application end ----------
You are defining the initial state right here :
function todos(state = [], action) {
Generally, while defining reducers, we also define initialState(state = [] in your case) , which is the state that goes into the reducer till we populate it with data (from an external source like api, or user input).
You can read more on initial state here : https://redux.js.org/recipes/structuring-reducers/initializing-state#initializing-state
there are two ways where you can define initial state;
the first one is in your reducer where you did function
todos(state = [], action) and ,
the second is when you create the store, you can pass initial state as a second argument in your createStore function. In your case , you have a second argument when you create your store which is an array of three todos which you can see when you console log it. store = Redux.createStore(todoApp, initialState), here the reducer gets this initial state

Resources