How do I map different arrays based on state? - javascript

I currently have a toggle button. When false, it should hide the red cards (late jobs) in it's column. When true, all jobs should be shown. However it is not working. The state changes, but no re render happens.
const App = () => {
const [showLateJobs, setShowLateJobs] = useState(true);
const [user, setUser] = useState("default");
const multipleUsersArray = [...otherUser, ...AllJobs];
let toOrderArray =
user === "default"
? AllJobs.filter((job) => job.category === "to_order")
: multipleUsersArray.filter((job) => job.category === "to_order");
let toOrderArrayNoLate =
user === "default"
? AllJobs.filter((job) => job.category === "to_order" && job.late != true)
: multipleUsersArray.filter(
(job) => job.category === "to_order" && job.late != true
);
const [toOrderState, setToOrderState] = useState(
showLateJobs ? toOrderArray : toOrderArrayNoLate
);
return (
<>
<Toggle
defaultChecked={true}
onChange={(e) => {
if (!e.target.checked) {
setShowLateJobs(false);
console.log("toggled");
} else {
setShowLateJobs(true);
}
}}
/>
<Column>
{toOrderState.map((job, index) => (
<JobCard
job_number={job.jobNumber}
time={job.time}
cardHeight={layout === "extended" ? "150px" : "50px"}
layout={layout}
backgroundColor={job.late && "#D64045"}
displayLateIcon={job.late && "block"}
/>
))}
</Column>
</>
);
};

The issue is that you're setting your computed values into state as initial values. Since you're never calling setToOrderState(...), the state value never changes.
If you'd like to derive new state from existing state, you can use a memo hook
// Define these outside your component
// so they're not re-initialised every render
const DEFAULT_USER = "default";
const CATEGORY_FILTER = "to_order";
const multipleUsersArray = [...otherUser, ...AllJobs];
const filterPredicate =
(includeLate) =>
({ category, late }) =>
category === CATEGORY_FILTER && (includeLate || !late);
// And these inside your component
const [showLateJobs, setShowLateJobs] = useState(true);
const [user, setUser] = useState(DEFAULT_USER);
const toOrderState = useMemo(
() =>
(user === DEFAULT_USER ? AllJobs : multipleUsersArray).filter(
filterPredicate(showLateJobs)
),
[user, showLateJobs] // hook dependencies
);

Related

localStorage is working wrong in my application

I have a problem with the localStorage in my application. When I add items to a list of "favorites" they are stored without any problem in the localStorage, they can even be deleted by clicking them again.
But when I refresh the page, my application doesn't read that these items are in the favorites list and therefore doesn't mark them. Also, when I add a new item to the favorites list it causes it to delete everything from localStorage and start over.
Here's a gif of the localStorage view
Here's the code:
import React, { useState, useEffect } from 'react';
import SearchBar from '../../SearchBar/SearchBar.js';
import FiltersBox from '../FiltersBox/FiltersBox.js';
import { getItems } from '../../../Database/Database.js';
import './ItemsContainer.css';
function ItemsContainer() {
const [items, setItems] = useState([]);
const [search, setSearch] = useState('');
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
const [sortPrice, setSortPrice] = useState('');
const [filterCategory, setFilterCategory] = useState('');
const addItemToFavorites = item => {
let existentItem = favoriteItems.find(favItem => favItem.id === item.id);
if (existentItem) {
let filterTheExistentItem = favoriteItems.filter(
favItem => item.title !== favItem.title
);
setFavoriteItems(filterTheExistentItem);
let stringItems = JSON.stringify(filterTheExistentItem);
localStorage.setItem('Favorite Items', stringItems);
} else {
setFavoriteItems([...favoriteItems, item]);
let stringItems = JSON.stringify([...favoriteItems, item]);
localStorage.setItem('Favorite Items', stringItems);
}
};
const filteredItemsList = () => {
let newItemList = [];
newItemList = items.filter(item => {
if (filterCategory !== '' && filterCategory !== 'none') {
return item.category === filterCategory;
} else {
return item;
}
});
if (sortPrice === 'ascending') {
return newItemList.sort((a, b) => (a.price > b.price ? 1 : -1));
} else if (sortPrice === 'descending') {
return newItemList.sort((a, b) => (b.price > a.price ? 1 : -1));
} else {
return newItemList;
}
};
function onSortSelected(sortValue) {
setSortPrice(sortValue);
}
function onCategorySelected(categoryValue) {
setFilterCategory(categoryValue);
}
useEffect(() => {
getItems().then(res => setItems(res));
}, []);
useEffect(() => {
let xd = JSON.parse(localItems);
console.log(xd);
}, [localItems]);
return (
<div>
<SearchBar setSearch={setSearch} />
<FiltersBox
items={items}
setItems={setItems}
onSortSelected={onSortSelected}
onCategorySelected={onCategorySelected}
/>
<div>
{filteredItemsList()
.filter(item =>
search.toLowerCase() === ''
? item
: item.title.toLowerCase().includes(search)
)
.map(item => (
<div key={item.id}>
<div>{item.title}</div>
<button
className={favoriteItems.includes(item) ? 'si' : 'no'}
onClick={() => addItemToFavorites(item)}>
Add to favorites
</button>
</div>
))}
</div>
</div>
);
}
export default ItemsContainer;
And here I leave a GIF with a continuous console.log of the localStorage:
I tried everyrhing, and I don't know what is happening.
You're retrieving your items in localItems and... you do nothing with this variable. You should initialize your state favoritesItems with your local storage
const getItemsFromLocalStorage = () => {
const items = localStorage.getItem('Favorite Items');
return items ? JSON.parse(items) : [];
}
const [favoriteItems, setFavoriteItems] = useState(getItemsFromLocalStorage())
This is where the culprit is:
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
You load localStorage into localItems, but you expect it to be in favoriteItems, where you have never assigned it. You would need to specify the item of localStorage as the initial state, like:
let localItems = localStorage.getItem('Favorite Items');
const [favoriteItems, setFavoriteItems] = useState(localItems ? localItems : []);

why the changing state of my component do not transmit when i export this component?

I have a hook that rules my requests. It has state "loading" that becoming true while loading
export const useHttp = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (url, method = 'GET', body = null, headers = {'Content-Type': 'application/json' }) => {
setLoading(true);
try {
const res = await fetch(url, {method, body, headers});
if (!res.ok) {
throw new Error(`Could not fetch ${url}, status: ${res.status}`);
}
const data = await res.json();
setLoading(false);
return data;
} catch(e) {
setLoading(false);
setError(e.message);
throw e;
}
}, [])
const clearError = useCallback(() => {
setError(null);
}, [])
return {
loading,
error,
request,
clearError
}
}
Also i have a service that makes requests:
import { useHttp } from "../hooks/http.hook";
const useNasaService = () => {
const { loading, error, request, clearError } = useHttp();
const _apiBase = 'https://api.nasa.gov/';
const _apiKey = 'api_key=DEMO_KEY';
const getMissionManifest = async (rover) => {
const res = await request(`${_apiBase}mars-photos/api/v1/manifests/${rover}/?${_apiKey}`);
return _transformManifestData(res.photo_manifest);
}
const getImagesData = async (rover, sol, page = 1) => {
const res = await request(`${_apiBase}mars-photos/api/v1/rovers/${rover}/photos?sol=${sol}&page=${page}&${_apiKey}`);
return res.photos.map(_transformImagesData);
}
const _transformImagesData = (data) => {
return {
id: data.id,
sol: data.sol,
earthDate: data.earth_date,
path: data.img_src,
camera: data.camera.full_name,
rover: data.rover.name
}
}
const _transformManifestData = (data) => {
return {
landingDate: data.landing_date,
launchDate: data.launch_date,
maxDate: data.max_date,
maxSol: data.max_sol,
name: data.name,
photos: data.photos,
status: data.status,
totalPhotos: data.total_photos
}
}
return {
loading,
error,
clearError,
getImagesData,
getMissionManifest
}
}
export default useNasaService;
Finally i have a component that needs state "loading" for disabling the inputs.
The question is why "loading" is never getting true in this component:
import useNasaService from '../../services/useNasaService';
const RoverFilter = (props) => {
const { loading } = useNasaService();
console.log(loading); /* always false */
const onRadioChange = (e) => {
props.onRoverSelected(e.target.value);
props.onRoverClicked(e.target.value);
}
return (
<div className="roverFilter" >
<h2 className="roverFilter__title">Select rover</h2>
<div className="roverFilter__inputs">
<label htmlFor="curiosity">Curiosity</label>
<input disabled={loading} type="radio" name="rover-choise" id="curiosity" value="curiosity" onChange={onRadioChange}/>
<label htmlFor="opportunity">Opportunity</label>
<input disabled={loading} type="radio" name="rover-choise" id="opportunity" value="opportunity" onChange={onRadioChange}/>
<label htmlFor="spirit">Spirit</label>
<input disabled={loading} type="radio" name="rover-choise" id="spirit" value="spirit" onChange={onRadioChange}/>
<label htmlFor="perseverance">Perseverance</label>
<input disabled={loading} type="radio" name="rover-choise" id="perseverance" value="perseverance" onChange={onRadioChange}/>
</div>
</div>
)
}
export default RoverFilter;
By the way, in my app there are another components, where "loading" becoming true without any problems. I cant see the difference.
for example, here loading works good:
import { useEffect, useState } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import useNasaService from '../../services/useNasaService';
import ImageGallerySkeleton from '../imageGallerySkeleton/ImageGallerySkeleton';
import Spinner from '../spinner/Spinner';
import ErrorMessage from '../errorMessage/ErrorMessage';
import SliderModal from '../sliderModal/SliderModal';
const ImageGallery = (props) => {
const {loading, getImagesData, clearError, error} = useNasaService();
const [imagesData, setImagesData] = useState([]);
const [nextPage, setNextPage] = useState(1);
const [firstLoading, setFirstLoading] = useState(true);
const [imagesDataLoaded, setImagesDataLoaded] = useState(false);
const [itemIndex, setItemIndex] = useState(0);
const [sliderOpen, setSliderOpen] = useState(false);
const transitionDuration = 1000;
const onImagesDataLoaded = (newData) => {
setImagesData(data => [...data, ...newData]);
setNextPage(page => page + 1);
setFirstLoading(false);
setImagesDataLoaded(true);
}
const onRequestImages = (rover, sol, page) => {
clearError();
if (!rover || !sol) return;
getImagesData(rover, sol, page)
.then(onImagesDataLoaded);
}
const onSliderClosed = () => {
setSliderOpen(false);
}
useEffect(() => {
onRequestImages(props.selectedRover, props.selectedSol, nextPage);
// eslint-disable-next-line
}, [props.selectedRover, props.selectedSol])
if (sliderOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "visible";
}
function renderItemList(arr) {
const itemList = arr.map((item, i) => {
return (
<CSSTransition
key={item.id}
in={imagesDataLoaded}
timeout={transitionDuration}
classNames='imageGallery__card'>
<li className="imageGallery__card"
onClick={() => {
setSliderOpen(true);
setItemIndex(i);
}}>
<img src={item.path} alt="img from mars"/>
<div className="imageGallery__descr">
<ul>
<li>Rover: {item.rover}</li>
<li>Earth_date: {item.earthDate}</li>
<li>Sol: {item.sol}</li>
<li>{item.camera}</li>
</ul>
</div>
</li>
</CSSTransition>
)
})
return (
<ul className="imageGallery__list">
<TransitionGroup component={null}>
{itemList}
</TransitionGroup>
</ul>
)
}
const spinner = loading && firstLoading ? <Spinner/> : null;
const skeleton = imagesData.length === 0 && firstLoading && !loading && !error ? <ImageGallerySkeleton/> : null;
const items = renderItemList(imagesData);
const errorMessage = error ? <ErrorMessage/> : null;
const counter = imagesData.length === 0 || error ? null :
<h2 className="imageGallery__title">
Showed {loading ? "..." : imagesData.length} photos of {props.totalPhotosInSol}
</h2>
const button = props.totalPhotosInSol === imagesData.length ? null :
<button
onClick={() => onRequestImages(props.selectedRover, props.selectedSol, nextPage)}
disabled={loading}
className="imageGallery__btn">{loading ? "Loading..." : "Load next page" }
</button>
const slider = <SliderModal
open={sliderOpen}
items={imagesData}
slideIndex={itemIndex}
onSliderClosed={onSliderClosed} />
const wrapStyles = firstLoading && loading ? {"padding": "50px"} : null;
return (
<section className="imageGallery" style={wrapStyles}>
{counter}
{spinner}
{skeleton}
{imagesData.length === 0 && !firstLoading ?
<h2 className="imageGallery__title">There is no photo for this sol</h2> :
items
}
{button}
{errorMessage}
{slider}
</section>
)
}
export default ImageGallery;
When you call useState in two different components, those states are independant from eachother. This is still true if you move the useState calls inside a custom hook. If two components call useNasaService (which calls useHttp, which calls useState), then the two components are creating their own states and own functions. If component A starts loading data, that will have no effect on component B.
So ImageGallery is working because it makes a call to getImagesData. This sets the loading state of ImageGallery to true. No other components are affected by this though. When the loading finishes, ImageGallery will set state to have the new data, but again, no other components can use this. RoverFilter on the other hand never calls getImagesData, so its loading state stays false, and it never gets any data.
In react, the typical way to share data is to lift state up. You have a component higher up in the tree, which is responsible for loading the data and setting state. That component then passes the data and functions down to any children that need it. You can either pass the data down using props, or if you need to pass the data a long distance you can consider using context instead.
There's also a wide variety of 3rd party libraries which can be used to manage global state. For example, Redux, Jotai, Zustand, MobX. These can make it simpler to share data between components in far-flung parts of the component tree.

How to clear n number of input fields dynamically on a single button click in react

const { useState, useEffect } = React;
const Thingy = ({ ...props }) => {
const [tenure, setTenure] = useState(null);
// state to hold tenure-dates (array of varying size)
const [tnDates, setTnDates] = useState(null);
const handleTenureChange = ev => setTenure(ev.target.value);
useEffect(() => setTnDates(
(tenure && tenure > 0)
? ([...Array(+tenure).keys()].map(
id => ({ id, tenureDate: '' })
)) : null
), [tenure]);
const handleDateChange = ev => {
const idx = ev.target.id;
const val = ev.target.value;
setTnDates(prev => {
const nd = [...prev];
nd[idx].tenureDate = val;
return nd;
});
};
above is the snippet for rendering tenure number of tenuredata where tenure is input
from user.
i want to clear all the tenure data input fields on a single button click. please help on this.
Since the input fields are presumably the states of both
const [tenure, setTenure] = useState(null);
const [tnDates, setTnDates] = useState(null);
To 'clear' the above state you need to reset it to it's original falsy value.
Since both the states you want to set are initially set to null you simply need to set their state to null again.
// button that clears the form fields
<button onClick={() => {
setTenure(null);
setTnDates(null);
}
Make sure you are using controlled components.
const handleClearInputs = () => {
setTenure(null)
setTnDates(null);
}
return(
<input value={tenure} />
<input value={tnDate} />
<button onClick={handleClearInputs}>Clear</button>
)

ReactJS: State is not changing on calling setLoading

when addItems() function is called, I changed the loading state to true. but it always remains false. what I am missing here?
const Persons = props => {
const [data, setData] = useState([]);
useEffect(() => {
addItems();
}, []);
let [position, setPosition] = useState(
(props && props.match && props.match.params.placeholder) || 0
);
const [loading, setLoading] = useState(false);
const numOfItem = 4;
const addItems = () => {
setLoading(true);
const items = [...data];
for (let i = 0; i < numOfItem; i++) {
const newItem = getData[position];
// if no item is found than go out from that loop
if (!newItem) break;
position++;
items.push(newItem);
}
setData(items);
setPosition(position);
setLoading(false);
};
return (
<>
{data.map(p => (
<Person person={p} />
))}
<button onClick={addItems}>Quick Load More {numOfItem} items</button>
</>
);
};
in devtools, I see loading state is always remains false.
You might need to start the loading state as true and after set all the items change the loading state to false.

Input value resets when input is cleared

I have an input in a react component to store a name:
<input key="marker-name" id="marker-name" name="marker-name" onChange={handleRename} type="text" value={name} />
I have written the following handler for it:
const handleRename = ({ target }) => {
setPerception({
...perception,
name: target.value
})
}
However, it's not working as expected, if a user tries to delete the existing name then as soon as the last character in the input is deleted (i.e. the input is empty) the previous value just pops back in.
Here is the full code of the component:
import React, { useState, useEffect } from 'react'
// import custom styles for child component
import './styles.scss'
const MarkerName = ({ store, onStoreUpdate, callbackFunction }) => {
const [clicked, setClicked] = useState(false)
const [perception, setPerception] = useState(null)
const [currentMarkerName] = useState(store.currentMarkerName)
const [currentMarkerForce] = useState(store.currentMarkerForce)
const [currentForce] = useState(store.currentForce)
// A copy of the store to capture the updates
const newStore = store
// Only populate the perception state if it's store value exists
useEffect(() => {
store.perception && setPerception(store.perception)
}, [])
// Only show the form to non-umpire players who cannot see the correct name
const clickHander = () =>
currentForce !== 'umpire' &&
currentForce !== currentMarkerForce &&
setClicked(true)
const handleRename = ({ target }) => {
setPerception({
...perception,
name: target.value
})
newStore.perception.name = target.value
onStoreUpdate(newStore)
}
const handleSubmit = e => {
e && e.preventDefault()
callbackFunction(newStore)
}
const handleRevert = e => {
e.preventDefault()
setPerception({
...perception,
name: null
})
newStore.perception.name = null
onStoreUpdate(newStore)
handleSubmit()
}
const name = perception && perception.name ? perception.name : currentMarkerName
return (
<>
<h2 key="header" onClick={clickHander}>{name}</h2>
{
clicked &&
<div className="input-container marker-name">
<label>
Update asset name
<input key="marker-name" id="marker-name" name="marker-name" onChange={handleRename} type="text" value={name} />
</label>
<button type="submit" onClick={handleSubmit}>Rename</button>
<button onClick={handleRevert}>Revert</button>
</div>
}
</>
)
}
export default MarkerName
As far as I can tell this line is the problem:
const name = perception && perception.name ? perception.name : currentMarkerName;
You are re-rendering on every character change (onChange={handleRename}). As soon as all characters are deleted perception && perception.name is evaluated to true && false (empty strings are falsy) which is false. So the component is rendered with const name = currentMarkerName. As currentMarkerName hasn't changed yet, it is re-rendered with the old name.
Use this instead:
const name = perception && typeof perception.name !== 'undefined' ? perception.name : currentMarkerName;
In React forms are controlled components, You were almost getting it but at that point where you checked for the value of perception before assigning to the inputValue...that does not seem right.
Could you try to make these changes and let us see the effect:
1. For the state value of perception, make the initial value any empty string:
const [perception, setPerception] = useState(null)
On the forminput use
The handleRename function could just be declared as
const handleRename = (e) => {e.target.name: e.target.value}

Categories

Resources