Correct item in array not always deleted. React context - reactjs

I have a delete function in my Context that I'm passing ids to so I can delete components from the array however it doesn't always work correctly. If I add 3 note components to the board for example, it will always delete the last item on the board. If I add a to do list in between 2 notes, they'll delete correctly. There are 2 console logs and the deleted one shows the correct deleted item, and components shows the 2 that are left. Again, if there are 3 notes, it deletes the last item everytime, but if I do one note, one to do, then one note again, the correct item on the board is deleted.
import React, { createContext, useReducer, useState } from "react";
import ComponentReducer from "./ComponentReducer";
const NewComponentState: NewComponentsState = {
components: [],
addComponent: () => {},
deleteComponent: () => {},
};
export const NewComponentContext =
React.createContext<NewComponentsState>(NewComponentState);
export const NewComponentProvider: React.FC = ({ children }) => {
const [components, setComponents] = useState(NewComponentState.components);
const deleteComponent = (id: any) => {
for (let i = 0; i < components.length; i++) {
if(components[i].id === id) {
let deleted = components.splice(i, 1)
console.log(deleted)
setComponents([...components])
console.log(components)
}
}
}
const addComponent = (newComponent: any) => {
setComponents([...components, newComponent])
}
return (
<NewComponentContext.Provider
value={{ components, deleteComponent, addComponent }}
>
{children}
</NewComponentContext.Provider>
);
};
Board component
import React, { useContext } from "react";
import { NewComponentContext } from "../Context/NewComponentContext";
import NewComponentMenu from "./NewComponents/NewComponentMenu";
import Note from "./NewComponents/Note/Note";
import Photo from "./NewComponents/Photo/Photo";
import TodoList from "./NewComponents/Todo/TodoList";
const newComponents: any = {
1: TodoList,
2: Photo,
3: Note
}
const Board = () => {
const { components } = useContext(NewComponentContext);
const componentList = components.map((component, i) => {
const id: number = component.componentType
const NewComponent = newComponents[id]
for (const property in newComponents) {
const value = parseInt(property)
if (value == id) {
return (
<div key={i}>
<NewComponent id={component.id}/>
</div>
)
}
}
});
return (
<div className="flex space-x-10 mt-8">
<NewComponentMenu />
<div className="grid grid-cols-6 gap-8">{componentList}</div>
</div>
);
};
export default Board;

Related

Can we use the same provider function in useContext() hook in react for two components?

I was implementing an e-commerce cart using react and inside it, I created a single context to get the category of the item user wants to see. I also used the react-router-dom to route a new component to display all the elements using the state of the context. But although I updated the state, it is showing that my state is empty.
function ProductCategory() {
const [categoryClick, setCategoryClick] = useState('LAPTOPS');
const {product, activeCategoryChanged} = useActiveCategoryContext();
const handleCategoryClick = (e) => {
setCategoryClick(e.target.id);
activeCategoryChanged(obj[categoryClick]);
}
const getProductCategoriesCard = () => {
return <ProductCategoryCardWrapper onClick={handleCategoryClick}>
{
PRODUCT_CATEGORIES.map((category, index) => {
return <ProductCards id = {category.name} key={index} style={{ backgroundImage : "url("+category.imageURL+")"}}><Heading id = {category.name}><Link to ='/ecom/category'>{category.name}</Link></Heading></ProductCards>
})
}
</ProductCategoryCardWrapper>
}
return (
<section>
{getProductCategoriesCard()}
{/* <Products product = {obj[categoryClick]}/> */}
</section>
)
}
Now below is the code sample of the context:
import React, {useContext, useState, useEffect} from 'react';
const ActiveCategoryContext = React.createContext();
export function useActiveCategoryContext() {
return useContext(ActiveCategoryContext);
}
export function ActiveCategoryContextProvider({children}){
const [product, setProduct] = useState([]);
const activeCategoryChanged = (active) => {
setProduct(active);
}
const value = {
product,
setProduct,
activeCategoryChanged
}
return <ActiveCategoryContext.Provider value = {value}>
{children}
</ActiveCategoryContext.Provider>
}
Now finally I am going to attach the code sample of the product component which uses the product state of the context to display all the elements inside the particular category selected by the user:
function Products() {
const {product} = useActiveCategoryContext();
console.log(product);
const getProductItemsDisplayed = () => {
return product.map((product, index) => (
<ProductCartCard key={index} product={product} />
));
};
return <TopSection>
{getProductItemsDisplayed()}
</TopSection>;
}

How to use state on one element of map on typescript?

I want to use onClick on one element of my map and set "favorite" for it. Basically, I'm trying to change the SVG of a Icon to the filled version, but with the map, all of items are changing too.
I already try to pass this to onClick, but doesn't work.
My code:
import React, { Component, useState, useEffect } from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { ForwardArrow } from "../../../assets/images/ForwardArrow";
import { BackArrow } from "../../../assets/images/BackArrow";
import * as S from "./styled";
import { IconFavoriteOffer } from "../../../assets/images/IconFavoriteOffer";
import { Rating } from "../../../assets/images/Rating";
import { TruckFill } from "../../../assets/images/TruckFill";
import { OpenBox } from "../../../assets/images/OpenBox";
import { IconCartWht } from "../../../assets/images/IconCartWht";
import axios from "axios";
import { off } from "process";
import SwitcherFavorite from "../SwitcherFavorite";
export default function Carousel() {
const [offers, setOffers] = useState<any[]>([]);
useEffect(() => {
axios.get("http://localhost:5000/offers").then((response) => {
setOffers(response.data);
});
}, []);
const [favorite, setFavorite] = useState(true);
const toggleFavorite = () => {
setFavorite((favorite) => !favorite);
};
return (
<>
<Slider {...settings}>
{offers.map((offer, index) => {
return (
<S.Offer key={index}>
<>
<S.OfferCard>
<S.OfferCardTop>
<S.OfferContentTop>
<S.OfferFavorite>
<S.OfferFavoriteButton onClick={toggleFavorite}> // Want to get this element of mapping
<SwitcherFavorite favorite={favorite} />
</S.OfferFavoriteButton>
</S.OfferFavorite>
<S.OfferStars>
<Rating />
</S.OfferStars>
</S.OfferContentTop>
</S.OfferCardTop>
</S.OfferCard>
</>
</S.Offer>
);
})}
</Slider>
</>
);
}
So, how can I do it?
Instead of using a single boolean flag with your current [favorite, setFavorite] = useState(false) for all the offers, which wouldn't work, you can store the list of offer IDs in an array. In this way you can also have multiple favourited offers.
Assuming your offer item has a unique id property or similar:
// This will store an array of IDs of faved offers
const [favorite, setFavorite] = useState([]);
const toggleFavorite = (id) => {
setFavorite((previousFav) => {
if (previousFav.includes(id)) {
// remove the id from the list
// if it already existed
return previousFav.filter(favedId => favedId !== id);
}
// add the id to the list
// if it has not been here yet
return [...previousFav, id]);
}
};
And then in your JSX:
/* ... */
<S.OfferFavoriteButton onClick={() => toggleFavorite(offer.id) }>
<SwitcherFavorite favorite={favorite.includes(offer.id)} />
// Similar to your original boolean flag to switch icons
</S.OfferFavoriteButton>
/* ... */

React Context - not setting state or reading from state correctly

I am using the react-beautiful-dnd(https://github.com/atlassian/react-beautiful-dnd) library for moving items around between columns. I am trying to update my columns in state after I drag an item to a different column. In the endDrag function I am logging out the columns variable right before setting state and it is correct at that point. So either I'm not setting state correctly, or I am not reading from state correctly in the column component. My console log in the OrderColumn component is outputting the old state of columns, so that seems to tell me I'm not setting the state correctly.
Here is my context file where I set the initial state:
import React, { createContext, useState } from 'react';
export const ScheduleContext = createContext();
export const ScheduleProvider = (props) => {
const orderData = [
['First', 'First Order'],
['Second', 'Second Order'],
['Third', 'Third Order'],
['Fourth', 'Fourth Order'],
['Fifth', 'Fifth Order']
];
const tempOrders = orderData.map(function(val, index) {
return {
id: index,
title: val[0] + ' Order',
desc: 'This order is for ' + val[1] + '.'
};
});
const orderIDs = tempOrders.map(function(val) {
return val.id;
});
const columnCount = 5;
const cols = [];
for (let i = 0; i < columnCount; i++) {
cols.push({
title: 'Line ' + i,
columnID: 'column-' + i,
orderIDs: i === 0 ? orderIDs : []
});
}
// eslint-disable-next-line no-unused-vars
const [orders, setOrders] = useState(tempOrders);
// eslint-disable-next-line no-unused-vars
const [columns, setColumns] = useState(cols);
const contextValue = { orders, setOrders, columns, setColumns };
return (
<ScheduleContext.Provider value={contextValue}>
{props.children}
</ScheduleContext.Provider>
);
};
Next, this is my SchedulePage which is the top component under App.js.
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';
import OrderColumn from '../ordercolumn/OrderColumn';
import { ScheduleContext } from '../../schedule-context';
const Schedule = () => {
const { columns, setColumns } = useContext(ScheduleContext);
const onDragEnd = (result) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const column = columns.find(
(col) => col.columnID === source.droppableId
);
const orderIDs = Array.from(column.orderIDs);
orderIDs.splice(source.index, 1);
const newColumn = {
...column,
orderIDs: orderIDs
};
const ind = columns.indexOf(column);
columns.splice(ind, 1, newColumn);
console.log(columns);
setColumns(columns);
};
const columnsArray = Object.values(columns);
return (
<DragDropContext onDragEnd={onDragEnd}>
<div className={'full-width'}>
<h1 className={'text-center'}>Schedule</h1>
<div className={'lines row no-gutters'}>
{columnsArray.map(function(val, index) {
return (
<OrderColumn
key={index}
title={val.title}
columnId={val.columnID}
orderIDs={val.orderIDs}
/>
);
})}
</div>
</div>
</DragDropContext>
);
};
Schedule.propTypes = {
orders: PropTypes.array
};
export default Schedule;
Finally, the OrderColumn page which is under the SchedulePage:
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Droppable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import Order from '../order/Order';
import { Scrollbars } from 'react-custom-scrollbars';
import { ScheduleContext } from '../../schedule-context';
import '../../App.css';
const MyOrder = styled.div``;
const OrderColumn = (props) => {
const colId = props.columnId;
const orders = useContext(ScheduleContext).orders;
const orderIDs = useContext(ScheduleContext).columns.find(
(col) => col.columnID === colId
).orderIDs;
console.log('orderIDs: ');
console.log(orderIDs);
return (
<Droppable droppableId={colId}>
{(provided) => {
console.log('orderIDs: ');
console.log(orderIDs);
return (
<MyOrder
className={'col order-column'}
ref={provided.innerRef}
{...provided.droppableProps}
>
<Scrollbars
// This will activate auto hide
autoHide
// Hide delay in ms
autoHideTimeout={1000}
// Duration for hide animation in ms.
autoHideDuration={200}
>
<h3 className={'text-center title'}>{props.title}</h3>
<div className={'orders'}>
{orderIDs &&
orderIDs.map((orderID, index) => {
const order = orders.find(
(o) => o.id === orderID
);
return (
<Order
key={orderID}
order={order}
index={index}
/>
);
})}
</div>
</Scrollbars>
{provided.placeholder}
</MyOrder>
);
}}
</Droppable>
);
};
OrderColumn.propTypes = {
orders: PropTypes.array,
title: PropTypes.string.isRequired,
columnId: PropTypes.string.isRequired
};
export default OrderColumn;
Figured out how to get my whole app online for testing/viewing if that helps anyone: https://codesandbox.io/s/smoosh-shape-g8zsp
As far as I understood, you change your columns and then save it to the state. Your problem is that you are just mutating your state:
columns.splice(ind, 1, newColumn); // <-- mutation of columns array
console.log(columns);
setColumns(columns); // <-- noop as react compares references of the arrays
To prevent this issue, you should just create a copy first and then modify and save it. React then will pick up the change:
const adjustedColumns = columns.slice(); // or Array.from(columns)
adjustedColumns.splice(ind, 1, newColumn);
setColumns(adjustedColumns);

Redux state is updated but component state not updated

how is it that my Redux state is updated, and can be log out in the pokelist.js file,
but my state variable is not set properly, is cardList is still an empty array, how do I
set the state properly? I log out the collection in the pokelist.js file, which logs out
an empty array first then an array containing the elements.
// reducer.js file
import { GET_LIMIT_NAMES } from '../actions/PokedexActions';
const initialState = {
collection: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_LIMIT_NAMES:
return {
collection: action.data
};
default:
return state;
}
};
//===================================================================================================
// action.js file
import Pokemon from '../Pokemon';
export const GET_LIMIT_NAMES = "GET_LIMIT_NAMES";
export const getLimitNames = (limit = 100) => {
// redux-thunk
return async dispatch => {
try {
const allResponse = await fetch(`https://pokeapi.co/api/v2/pokemon/?limit=${limit}`);
const allUrlsData = await allResponse.json();
// console.log(allUrlsData.results);
const collection = [];
Promise.all(allUrlsData.results.map(urlData => {
var pokemon;
fetch(urlData.url).then(resp =>
resp.json()
).then(data => {
// console.log(data);
pokemon = new Pokemon(data);
// pokemon.log();
collection.push(pokemon)
}).catch(err => {
console.log(err);
})
return collection;
}))
// console.log(collection)
dispatch({
type: GET_LIMIT_NAMES,
data: collection
});
} catch (err) {
console.log(err);
}
};
};
//===================================================================================================
// I want to make a list of cards from the Redux state
// pokelist.js
import React, { useState, useEffect } from 'react';
import Card from 'react-bootstrap/Card';
import ListGroup from 'react-bootstrap/ListGroup';
import { useSelector } from 'react-redux';
const PokeList = () => {
const [cardList, setCardList] = useState();
const collection = useSelector(state => state.pokedex.collection);
useEffect(() => {
console.log(collection)
setCardList(collection.map(pokeData =>
<Card key={pokeData.id} style={{ width: '18rem' }}>
<Card.Img variant="top" src={pokeData.sprite + '/100px180'} />
<Card.Body>
<Card.Title>{pokeData.Name}</Card.Title>
<ListGroup className="list-group-flush">
<ListGroup.Item>{'Height: ' + pokeData.height}</ListGroup.Item>
<ListGroup.Item>{'Weight: ' + pokeData.weight}</ListGroup.Item>
</ListGroup>
</Card.Body>
</Card>))
}, [collection])
return (
<div>
{cardList}
</div>
)
}
export default PokeList;
//===================================================================================================
// search.js file where i render the component and call the dispatch function
import React, { useState, useEffect } from 'react';
import { Container, Row, Col, Image, Button } from 'react-bootstrap';
import { useDispatch } from 'react-redux';
import PokeList from './pokedex/PokeList';
import * as pokedexActions from './pokedex/actions/PokedexActions';
const Search = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(pokedexActions.getLimitNames(5))
}, [dispatch])
return (
<div>
<Container>
<h2>Search</h2>
<PokeList />
</Container>
</div>
);
}
export default Search;
useState() hook is just a function call. It returns value and function pair. values is just a constant it doesn't have any property binding.
// Think of this line
const [cardList, setCardList] = useState([]);
// As these two lines
const cardList = [];
const setCardList = someFunction;
When you call setCardList your variable is not changed immediately, it doesn't have property binding. It just tells react to return new value on next render.
See my answer React Hooks state always one step behind.
In your case you can simply skip useState hook
const PokeList = () => {
const collection = useSelector(state => state.pokedex.collection);
const cardList = collection.map(
pokeData => (
<Card key={pokeData.id} style={{ width: '18rem' }}>
<Card.Img variant="top" src={pokeData.sprite + '/100px180'} />
<Card.Body>
<Card.Title>{pokeData.Name}</Card.Title>
<ListGroup className="list-group-flush">
<ListGroup.Item>{'Height: ' + pokeData.height}</ListGroup.Item>
<ListGroup.Item>{'Weight: ' + pokeData.weight}</ListGroup.Item>
</ListGroup>
</Card.Body>
</Card>)
);
return (
<div>
{cardList}
</div>
)
}

On child component state change, update siblings

Partially working example: https://codesandbox.io/s/jolly-smoke-ryb2d
Problem:
When a user expands/opens a component row, all other rows inside the rows parent component need to be collapsed. Unfortunately, I can't seem to get the other sibling rows to collapse.
I tried passing down a handler from the parent to the child that updates the state of the parent which would then in turn propagate down to the children.
Expected Result
On expand/open of a row, collapse any other rows that are open inside the parent component that isn't the one clicked
Code:
App.tsx
import React from "react";
import ReactDOM from "react-dom";
import Rows from "./Rows";
import Row from "./Row";
import "./styles.css";
export interface AppProps {}
const App: React.FC<AppProps> = props => {
return (
<Rows>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
</Rows>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Rows.tsx
Rows.tsx
import React, { useState, useEffect } from "react";
import Row, { RowProps } from "./Row";
export interface RowsProps {}
const Rows: React.FC<RowsProps> = props => {
const [areRowsHidden, setAreRowsHidden] = useState<boolean>(false);
useEffect(() => {});
const handleOnShow = (): void => {};
const handleOnCollapse = (): void => {};
const renderChildren = (): React.ReactElement[] => {
return React.Children.map(props.children, child => {
const props = Object.assign(
{},
(child as React.ReactElement<RowsProps>).props,
{
onShow: handleOnShow,
onCollapse: handleOnCollapse,
isCollapsed: areRowsHidden
}
);
return React.createElement(Row, props);
});
};
return <>{renderChildren()}</>;
};
export default Rows;
Row.tsx
import React, { useState, useEffect } from "react";
export interface RowProps {
onCollapse?: Function;
onShow?: Function;
isCollapsed?: boolean;
}
const Row: React.FC<RowProps> = props => {
const [isCollapsed, setIsCollapsed] = useState(props.isCollapsed || true);
useEffect(() => {}, [props.isCollapsed]);
const handleClick = (): void => {
if (isCollapsed) {
props.onShow();
setIsCollapsed(false);
} else {
props.onCollapse();
setIsCollapsed(true);
}
};
return (
<>
{React.cloneElement(props.children[0], {
onClick: handleClick
})}
{isCollapsed ? null : React.cloneElement(props.children[1])}
</>
);
};
export default Row;
I would store which row is open inside of Rows.tsx and send that value down to its children rather than having the child control that state. You may see this being referred to as lifting state up. You can read more about it here.
Rows.tsx
import React, { useState } from 'react'
import Row from './Row'
export interface RowsProps {}
const Rows: React.FC<RowsProps> = props => {
const [visibleRowIndex, setVisibleRowIndex] = useState<number>(null)
const renderChildren = (): React.ReactElement[] => {
return React.Children.map(props.children, (child, index) => {
const props = Object.assign({}, (child as React.ReactElement<RowsProps>).props, {
onShow: () => setVisibleRowIndex(index),
onCollapse: () => setVisibleRowIndex(null),
isCollapsed: index !== visibleRowIndex
})
return React.createElement(Row, props)
})
}
return <>{renderChildren()}</>
}
export default Rows
Row.tsx
import React from 'react'
export interface RowProps {
onCollapse?: Function
onShow?: Function
isCollapsed?: boolean
}
const Row: React.FC<RowProps> = props => {
const handleClick = (): void => {
if (props.isCollapsed) {
props.onShow()
} else {
props.onCollapse()
}
}
return (
<>
{React.cloneElement(props.children[0], {
onClick: handleClick
})}
{props.isCollapsed ? null : React.cloneElement(props.children[1])}
</>
)
}
export default Row
Example: https://codesandbox.io/s/gifted-hermann-oz2zw
Just a side note: I noticed you're cloning elements and doing something commonly referred to as prop drilling. You can avoid this by using context if you're interested although not necessary.

Resources