I'm trying to figure out how to correctly use React Context. I'm hung up on this issue of trying to access the Context from outside the function component. I'm getting the error:
Line 9:18: React Hook "useContext" is called in function "onDragEnd" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
Here is my entire Schedule js file:
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 onDragEnd = (result) => {
const { destination, source, draggableId } = result;
const context = useContext(ScheduleContext); // <-- issue is here
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const column = context.columns[source.droppableId];
const orderIDs = Array.from(column.orderIDs);
orderIDs.splice(source.index, 1);
orderIDs.splice(destination.index, 0, draggableId);
const newColumn = {
...column,
orderIDs: orderIDs
};
const newColumns = {
...context.columns,
newColumn
};
context.setColumns(newColumns);
};
const Schedule = () => {
const { orders, setOrders, columns, setColumns } = useContext(
ScheduleContext
);
return (
<DragDropContext onDragEnd={onDragEnd}>
<div className={'full-width'}>
<h1 className={'text-center'}>Schedule</h1>
<div className={'lines row no-gutters'}>
{columns.map(function(val, index) {
if (index === 0) {
return (
<OrderColumn
title={val.title}
columnId={index}
orders={orders}
setOrders={setOrders}
setColumns={setColumns}
/>
);
} else {
return (
<OrderColumn
title={val.title}
columnId={index}
setOrders={setOrders}
setColumns={setColumns}
/>
);
}
})}
</div>
</div>
</DragDropContext>
);
};
Schedule.propTypes = {
orders: PropTypes.array
};
export default Schedule;
Not to sound glib, but essentially it means exactly what it says. onDragEnd is not a React component because it is not returning a ReactElement or some kind of JSX. If you edited your blank return statements to return <div>'s (for all paths) it would be considered a component and work properly, but as of right now it's not returning anything.
Using useCallback and passing the context to that function will help solve your problem.
An example below:
const onDragEnd(result, context) => {
// Adjust as necessary
}
const Schedule = () => {
const context = useContext(ScheduleContext);
const onDragEnd = useCallback((result) => onDragEnd(result, context), [context]);
return <DragDropContext onDragEnd={onDragEnd}>
You can also either inline onDragEnd or pull out more and make a custom hook.
Related
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>;
}
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>
/* ... */
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;
I've seen examples of the useObserver hook that look like this:
const Test = () => {
const store = useContext(storeContext);
return useObserver(() => (
<div>
<div>{store.num}</div>
</div>
))
}
But the following works too, and I'd like to know if there's any reason not to use useObserver to return a value that will be used in render rather than to return the render.
const Test = () => {
const store = useContext(storeContext);
var num = useObserver(function (){
return store.num;
});
return (
<div>
<div>{num}</div>
</div>
)
}
Also, I don't get any errors using useObserver twice in the same component. Any problems with something like this?
const Test = () => {
const store = useContext(storeContext);
var num = useObserver(function (){
return store.num;
});
return useObserver(() => (
<div>
<div>{num}</div>
<div>{store.num2}</div>
</div>
))
}
You can use observer method in the component. And use any store you want.
import { observer } from "mobx-react-lite";
import { useStore } from "../../stores/StoreContext";
const Test = observer(() => {
const { myStore } = useStore();
return() => (
<div>
<div>{myStore.num}</div>
<div>{myStore.num2}</div>
</div>
)
}
);
StoreContext.ts
import myStore from './myStore'
export class RootStore{
//Define your stores here. also import them in the imports
myStore = newMyStore(this)
}
export const rootStore = new RootStore();
const StoreContext = React.createContext(rootStore);
export const useStore = () => React.useContext(StoreContext);
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.