I am using useContext as a global state solution. I have a Store.jsx which contains my state, and a reducer.jsx which reduces. I am using Konva to create some shapes on an HTML5 Canvas. My goal is when I click on a shape I want to update my global state with a reference to what is active, and when I click again, to clear the reference.
My Full Code can be found here:
https://codesandbox.io/s/staging-platform-2li83?file=/src/App.jsx
Problem:
The problem is when I update the global state via the onClick event of a shape, its says that the reference is 'null', but when I console.log the reference in the onClick I can see the correct reference.
I think I am missing an important point to how useRef works.
This is how the flow appears in my head when I think about this:
I create a canvas, and I map an array of rectangle properties. This creates 4 rectangles. I use a wrapper component that returns a rectangle.
{rectArray.map((rectangle, index) => {
return (
<RectWrapper key={index} rectangle={rectangle} index={index} />
);
})}
Inside the RectWrapper, I create a reference, pass it to the ref prop of the Rect. In the onclick function, when I console log 'shapeRef' I see the refence ONLY when dispatch is commented out. If I uncomment dispatch then it shows as null, and if I console log the state, the reference is always null.
const RectWrapper = ({ rectangle, index }) => {
const shapeRef = React.useRef();
return (
<Rect
x={rectangle.x + index * 100}
y={5}
width={50}
height={50}
fill="red"
ref={shapeRef}
onClick={() => {
console.log("ShapeRef: ");
console.log(shapeRef); // This correctly identifies the rect only when dispatch is uncommented
dispatch({
type: "active_image",
payload: {
index: index,
reference: shapeRef
}
});
}}
/>
);
};
perhaps I am going about this to wrong way with hooks. I am just trying to keep a global state of whats been clicked on because components in another file would rely on this state.
The problem is happening because you are creating RectWrapper component as a functional component within your App component causing a new reference of the component to be created again and again and thus the reference is lost
Move your RectWrapper into a separate component declared outside of App component and pass on dispatch as a prop to it
import React, { useEffect, useContext, useState, Component } from "react";
import { Stage, Layer, Rect, Transformer } from "react-konva";
import { Context } from "./Store.jsx";
import "./styles.css";
const RectWrapper = ({ rectangle, index, dispatch }) => {
const shapeRef = React.useRef();
return (
<Rect
x={rectangle.x + index * 100}
y={5}
width={50}
height={50}
fill="red"
ref={shapeRef}
onClick={() => {
console.log("ShapeRef: ");
console.log(shapeRef);
dispatch({
type: "active_image",
payload: {
index: index,
reference: shapeRef
}
});
}}
/>
);
};
export default function App() {
const [state, dispatch] = useContext(Context);
console.log("Global State:");
console.log(state);
const rectArray = [
{ x: 10, y: 10 },
{ x: 10, y: 10 },
{ x: 10, y: 10 },
{ x: 10, y: 10 }
];
return (
<div className="App">
<Stage height={500} width={500}>
<Layer>
{rectArray.map((rectangle, index) => {
return (
<RectWrapper
dispatch={dispatch}
key={index}
rectangle={rectangle}
index={index}
/>
);
})}
</Layer>
</Stage>
</div>
);
}
Working demo
I don't think you need to create a ref in RectWrapper, because onClick has one event parameter. And the ref of the element that was clicked can be found in the event:
onClick={(e) => {
const thisRef = e.target;
console.log(thisRef );
...
Here is a working version without useRef: https://codesandbox.io/s/peaceful-brook-je8qo
Related
I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.
I made a context to share the value of the variable "clicked" throughout my nextjs pages, it seems to give no errors but as you can see the variable's value remains FALSE even after the click event. It does not change to TRUE. This is my first time working with context, what am I doing wrong?
I'm using typescript
PS: After the onClick event the log's number shoots up by 3 or 4, is it being executed more than once, but how?
controlsContext.tsx
import { createContext, FC, useState } from "react";
export interface MyContext {
clicked: boolean;
changeClicked?: () => void;
}
const defaultState = {
clicked: false,
}
const ControlContext = createContext<MyContext>(defaultState);
export const ControlProvider: FC = ({ children }) => {
const [clicked, setClicked] = useState(defaultState.clicked);
const changeClicked = () => setClicked(!clicked);
return (
<ControlContext.Provider
value={{
clicked,
changeClicked,
}}
>
{children}
</ControlContext.Provider>
);
};
export default ControlContext;
Model.tsx
import ControlContext from "../contexts/controlsContext";
export default function Model (props:any) {
const group = useRef<THREE.Mesh>(null!)
const {clicked, changeClicked } = useContext(ControlContext);
const handleClick = (e: MouseEvent) => {
//e.preventDefault();
changeClicked();
console.log(clicked);
}
useEffect(() => {
console.log(clicked);
}, [clicked]);
useFrame((state, delta) => (group.current.rotation.y += 0.01));
const model = useGLTF("/scene.gltf ");
return (
<>
<TransformControls enabled={clicked}>
<mesh
ref={group}
{...props}
scale={clicked ? 0.5 : 0.2}
onClick={handleClick}
>
<primitive object={model.scene}/>
</mesh>
</TransformControls>
</>
)
}
_app.tsx
import {ControlProvider} from '../contexts/controlsContext';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ControlProvider>
<Component {...pageProps}
/>
</ControlProvider>
)
}
export default MyApp
Issues
You are not actually invoking the changeClicked callback.
React state updates are asynchronously processed, so you can't log the state being updated in the same callback scope as the enqueued update, it will only ever log the state value from the current render cycle, not what it will be in a subsequent render cycle.
You've listed the changeClicked callback as optional, so Typescript will warn you if you don't use a null-check before calling changeClicked.
Solution
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked && changeClicked();
}}
>
...
</mesh>
...
Or declare the changeClicked as required in call normally. You are already providing changeClicked as part of the default context value, and you don't conditionally include in in the provider, so there's no need for it to be optional.
export interface MyContext {
clicked: boolean,
changeClicked: () => void
}
...
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked();
}}
>
...
</mesh>
...
Use an useEffect hook in to log any state updates.
const { clicked, changeClicked } = useContext(ControlContext);
useEffect(() => {
console.log(clicked);
}, [clicked]);
Update
After working with you and your sandbox it needed a few tweaks.
Wrapping the index.tsx JSX code with the ControlProvider provider component so there was a valid context value being provided to the app. The UI here had to be refactored into a React component so it could itself also consume the context value.
It seems there was some issue with the HTML canvas element, or the mesh element that was preventing the Modal component from maintaining a "solid" connection with the React context. It wasn't overtly clear what the issue was here, but passing the context values directly to the Modal component as props resolved the issue with the changeClicked callback becoming undefined.
A few things -
setClicked((prev) => !prev);
instead of
setClicked(!clicked);
As it ensures it's not using stale state. Then you are also doing -
changeClicked
But it should be -
changeClicked();
Lastly, you cannot console.log(clicked) straight after calling the set state function, it will be updated in the next render
This is the parent Component.
export default function CanvasPanel(props) {
const [sketchCoordinates, setSketchCoordinates] = useState([]);
useEffect(() => {
console.log(sketchCoordinates);
}, [sketchCoordinates]);
const onAddPoint = function (mousePos) {
setSketchCoordinates([
...sketchCoordinates,
{ x: mousePos.x, y: mousePos.y }
]);
};
const onPrintObject = (toPrint) => {
console.log(toPrint);
};
return (
<div className="canvas-panel">
Points:
<hr />
{sketchCoordinates.join(", ")}
<Canvas
sketchCurveCoordinates={sketchCoordinates}
addPoint={onAddPoint}
printObject={onPrintObject}
/>
<Sidepanel createPoint={onAddPoint} />
</div>
);
}
This is the child Component
export default function Canvas(props) {
const getMousePos = function (event) {
const offset = canvasRef.current.getBoundingClientRect();
return {
x: event.clientX - offset.left,
y: event.clientY - offset.top
};
};
const handleOnMouseUp = (event) => {
const mousePos = getMousePos(event);
props.printObject(mousePos);
props.addPoint(mousePos);
};
return (
<div className="canvas-container">
<PureCanvas
handleOnMouseUp={handleOnMouseUp}
/>
</div>
);
};
This is the PureCanvas component
export default class PureCanvas extends Component {
shouldComponentUpdate() {
return false;
}
render() {
return (
<canvas
className="canvas"
width="750"
height="500"
onMouseUp={this.props.handleOnMouseUp}
></canvas>
);
}
}
The code is not complete as I removed all canvas related stuff because it doesn't really matter, but it has enough to explain and reproduce the behaviour. The addPoint function should append an object with x and y properties to the sketchCoordinates array through spread operator and the update function from useState. This works when calling addPoint({x: (any int), y: ({any int}) at the CanvasPanel component, but when calling at the Canvas component through props it works only once. The array appends the first element but then it just replaces that same element with a new one, when it should append that element. So, I know I could do this with classes but I can't understand why it won't work with hooks. The following image shows the console after some updates to de sketchCoordinates array:
Console
EDIT
Please consider this code sandbox for further enlightenment.
This doesn't appear to be an issue with anything specific to the hook: if we replace the <canvas> with a button that just calls handleOnMouseUp on click, it works as expected.
I suspect it's not anything to do with the hook, and perhaps it's some detail that's been simplified out of your example:
Here's a code sandbox with this simplified version, that replaces the canvas with just a button.
I try to implement a function in my app that allows the user to reset all the components that he dragged around to be reset to their original position.
I assume that this functionality exists in react-draggable because of this closed and released issue: "Allow reset of dragging position" (https://github.com/idanen/react-draggable/issues/7). However I did not find any hint in the documentation (https://www.npmjs.com/package/react-draggable).
There was one question with the same content in stackoverflow, but it has been removed (https://stackoverflow.com/questions/61593112/how-to-reset-to-default-position-react-draggable).
Thanks for your help :-)
The referenced issue on the GitHub references a commit. After taking a look at the changes made in this commit, I found a resetState callback added to the useDraggable hook. In another place in the commit, I found a change to the test file which shows usage of the hook.
function Consumer(props) {
const {
targetRef,
handleRef,
getTargetProps,
resetState,
delta,
dragging
} = useDraggable(props);
const { style = defaultStyle } = props;
return (
<main
className='container'
ref={targetRef}
data-testid='main'
style={style}
{...getTargetProps()}
>
{dragging && <span>Dragging to:</span>}
<output>
{delta.x}, {delta.y}
</output>
<button className='handle' ref={handleRef}>
handle
</button>
<button onClick={resetState}>reset</button>
</main>
);
}
The hook returns a set of callbacks, including this callback, which can be used to reset the state of the draggable.
I wanted the component to reset back to its original position when the component was dropped.
Using hooks I monitored if the component was being dragged and when it was false reset the position otherwise it would be undefined.
export default function DraggableComponent(props: any) {
const {label} = props
const [isDragging, setIsDragging] = useState<boolean>(false)
const handleStart = (event: any, info: DraggableData) => {
setIsDragging(true)
}
const handleStop = (event: any, info: DraggableData) => {
setIsDragging(false)
}
return (
<Draggable
onStart={handleStart}
onStop={handleStop}
position={!isDragging? { x: 0, y: 0 } : undefined}
>
<Item>
{label}
</Item>
</Draggable>
)
}
Simple approach would be:
creating a new component to wrap our functionality around the Draggable callbacks
reset position when onStop callback is triggered
Example:
import { useState } from 'react';
import Draggable, { DraggableData, DraggableEvent, DraggableProps } from 'react-draggable';
export function Drag({ children, onStop, ...rest }: Partial<DraggableProps>) {
const initial = { x: 0, y: 0 }
const [pos, setPos] = useState(initial)
function _onStop(e: DraggableEvent, data: DraggableData){
setPos(initial)
onStop?.(e, data)
}
return (
<Draggable position={pos} onStop={_onStop} {...rest}>
{children}
</Draggable>
)
}
Usage:
export function App() {
return (
<Drag> Drag me </Drag>
)
}
Note that this answer does not work.
None of these approaches worked for me, but tobi2424's post on issue 214 of the Draggable repo did. Here's a minimal proof-of-concept:
import React from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
// Fires when the user stops dragging the element
const choiceHandler = () => {
setDragPosition({x: 0, y: 0});
};
return (
<Draggable
onStop={choiceHandler}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;
Edit
The code above works intermittently but not particularly well. As far as I can work out, react-draggable stores data about the position of the dragged element somewhere outside of React, in order to preserve the position of the element between component refreshes. I was unable to determine how to reset the position of the element on command and none of the other example code solves the problem for me.
You can do this in a very haphazard manner. There may be another way to set state more safely on this but I didn't look too deeply into it.
import React from 'react';
export default class 😊 extends Component {
constructor(props) {
super(props);
this.draggableEntity = React.createRef();
}
resetDraggable() {
try {
this.draggableEntity.current.state.x = 0;
this.draggableEntity.current.state.y = 0;
} catch (err) {
// Fail silently
}
}
render() {
return (
<Draggable
ref={this.draggableEntity}
>
<img onClick={(e) => {this.resetDraggable()}}></img>
</Draggable>
)
}
}
There happens to be another way! You can use it's exposed ref element to reset its offset. This can be achieved like so:
import React, {useRef, useCallback} from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
const draggerRef = useRef(null);
// Fires when the user stops dragging the element
const resetDrag = useCallback(() => {
setDragPosition({x: 0, y: 0});
draggerRef.current?.setState({ x: 0, y: 0 }); // This is what resets it!
}, [setDragPosition, draggerRef]);
return (
<Draggable
ref={draggerRef}
onStop={resetDrag}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;
const { contacts, getContacts, } = useContext(
ContactContext
);
useEffect(() => {
getContacts();
//eslint-disable-next-line
});
//prettier-ignore
return (
<Fragment>
{contacts.length === 0 ? (
<h4 style={{ textAlign: 'center' }}>Please add a contact</h4>
): null}
{contacts.map(contact => (
<ContactItem contact={contact} />
))}
</Fragment>
);
};
contacts is initially an empty array and after getContacts makes the request to the server, it updates the contacts state
but somehow the words 'Please add a contact' is always showing even after getContacts() returns an array with a few contacts. it seems like it does not re-render that part of the component because when the component initially ran, contacts was an empty array
When it comes to "why will/won't my component update", React follows three simple rules. It will only "re-render" your component if:
its props change
its state changes
its context changes
For those last two it's critical that you change them correctly, using the appropriate setter method. For instance, if you are using state via hooks (vs. class-based state), ie.
const [foo, setFoo] = useState('');
You have to use setFoo:
setFoo(newValue);
If you simply change the Javascript variable:
foo = newValue;
React has no way of knowing about the change, and so your component won't re-render.
While you haven't shown all your code, it seems very likely you're changing your context directly, instead of using the appropriate setter function (as part of a state variable).
P.S. See https://reactjs.org/docs/context.html#updating-context-from-a-nested-component if you need clarification on the pattern of using state to control context.
Please use hook called useState
import { useState } from "react";
const { contacts, getContacts, } = useContext(
ContactContext
);
const [state, setState ] = useState({
contacts:[]
})
useEffect(() => {
setState({
contact:getContacts()
})
//eslint-disable-next-line
});
//prettier-ignore
return (
<Fragment>
{state.contacts.length === 0 ? (
<h4 style={{ textAlign: 'center' }}>Please add a contact</h4>
): null}
{state.contacts.map(contact => (
<ContactItem contact={contact} />
))}
</Fragment>
);
};
the only thing that worked was this, but I don't know why it did not work previously and why it does work now:
if(contacts.length === 0 ){
return <h4>Please add a contact</h4>
}
I put this line of code on top of the other return statement