I'm a Noob in a quest to learn the React Kung-Fu techniques.
I'm struggling to implement useContext to update a set of values from two sibling components
MainView.js
export function MainView() {
return(
<Fragment>
<canvas id='paper-canvas' resize='true' />
<ViewProvider>
<Sketch />
<PathControls />
</ViewProvider>
<Header />
</Fragment>
);
}
ViewContext.js
export const ViewContext = createContext([{}, () => {}]);
export const ViewProvider = (props) => {
const [isPathVisibleState, setIsPathVisibleState] = useState(true);
return(
<ViewContext.Provider value={[isPathVisibleState, setIsPathVisibleState]}>
{props.children}
</ViewContext.Provider>
);
}
PathControls.js
export default function PathControls(props) {
const [isPathVisibleState, setIsPathVisibleState] = useContext(ViewContext);
function handlePathVisibleChange() {
console.log(isPathVisibleState);
setIsPathVisibleState(isPathVisibleState => !isPathVisibleState);
}
return(
<div className='path-line-controls container fixed-bottom'>
<img src={pathLineIcon} alt='Show/Hide path line' title='Show/Hide path line' onClick={handlePathVisibleChange} />
</div>
);
}
Sketch.js
export default function Sketch(props) {
const [isPathVisibleState, setIsPathVisibleState] = useContext(ViewContext);
window.onload = function() {
// Paperjs initialization settings
paper.install(window);
paper.setup('paper-canvas');
// Creates a new path line that shows the connected dots
path = new Path();
path.strokeColor = 'black';
path.visible = isPathVisibleState;
view.onMouseDown = function(event) {
addDot(event.point);
console.log("MOUSEDOWN------","PATH:", isPathVisibleState);
}
}
function addDot(point) {
//...
}
return null;
}
My goal is to have PathControls component buttons to toggle a value isPathVisibleState true/false so the path drawn in the Sketch component visible property switch to true/false
My current setting does toggles isPathVisibleState true/false from the PathControls component but when I console that state variable from the Sketch component it always maintains the same initial value set in the Context component.
Any help will be much appreciated.
It seems Sketch doesn't re-render altought isPathVisibleState changes, try adding a side-effect using useEffect:
export default function Sketch(props) {
const [isPathVisibleState, setIsPathVisibleState] = useContext(ViewContext);
useEffect(() => {
window.onload = function() {
// Paperjs initialization settings
paper.install(window);
paper.setup("paper-canvas");
// Creates a new path line that shows the connected dots
path = new Path();
path.strokeColor = "black";
};
}, []);
useEffect(() => {
path.visible = isPathVisibleState;
view.onMouseDown = function(event) {
addDot(event.point);
console.log("MOUSEDOWN------", "PATH:", isPathVisibleState);
};
}, [isPathVisibleState]);
function addDot(point) {
//...
}
return null;
}
Note that as your code is written right now, Sketch body will be executed on each re-render, that's why I moved it to componentDidMount cycle in the first useEffect.
If it doesn't work, you should check why Sketch doesn't re-render, adding a sandbox it always a good step.
Related
When I use react aria's mergeProps to merge button props and then pass to my component it causes it to override (I'm guessing) the initial props, but not all. The background color won't appear but everything else does, and the styling works fine on hover or any secondary "conditional" props.
Code:
export default function Button(props: ButtonProps): ReactElement {
const p = { ...DEFAULT_PROPS, ...props };
const ref = useRef<HTMLButtonElement>(null);
const { buttonProps, isPressed } = useButton(p, ref);
const { hoverProps, isHovered } = useHover({ isDisabled: p.isDisabled });
const behaviorProps = mergeProps(buttonProps, hoverProps);
return (
<button
className={clsx([
'button-main',
{
'is-hovered': isHovered,
'is-pressed': isPressed,
'is-secondary': p.variant === 'secondary',
},
])}
{...behaviorProps}
>
{p.children}
</button>
);
}
For context, I have a web app that displays an image in a React-Bootstrap Container component (Arena) that holds an image where users are to look and find specific characters.
Separately, I created a div component (CustomCursor) where the background is set to a magnifying glass SVG image.
The Arena component tracks mouse position through an OnMouseMove handler function (handleMouseMove) and passes those coordinates as props to the CustomCursor component.
Here is my Arena component code:
import { useState, useEffect } from 'react';
import { Container, Spinner } from 'react-bootstrap';
import CustomCursor from '../CustomCursor/CustomCursor';
import Choices from '../Choices/Choices';
import { getImageURL } from '../../helpers/storHelpers';
import './Arena.scss';
export default function Arena(props) {
const [arenaURL, setArenaURL] = useState('');
const [loaded, setLoaded] = useState(false);
const [clicked, setClicked] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function handleClick(e) {
setClicked(true);
}
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
useEffect(() => {
retreiveArena();
// FUNCTION DEFINITIONS
async function retreiveArena() {
const url = await getImageURL('maps', 'the-garden-of-earthly-delights.jpg');
setArenaURL(url);
setLoaded(true);
}
}, [])
return (
<Container as='main' fluid id='arena' className='d-flex flex-grow-1 justify-content-center align-items-center' onClick={handleClick}>
{!loaded &&
<Spinner animation="border" variant="danger" />
}
{loaded &&
<img src={arenaURL} alt='The Garden of Earthly Delights triptych' className='arena-image' onMouseMove={handleMouseMove} />
}
{clicked &&
<Choices x={x} y={y} />
}
<CustomCursor x={x} y={y} />
</Container>
)
}
Here is my CustomCursor code:
import './CustomCursor.scss';
export default function CustomCursor(props) {
const { x, y } = props;
return (
<div className='custom-cursor' style={{ left: `${x - 64}px`, top: `${y + 50}px` }} />
)
}
When I first created the OnMouseMove handler function I simply set the x and y state values by passing them into their respective state setter functions directly:
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
However, I noticed this was slow and laggy and when I refactored this function to use setter functions instead it was much faster (what I wanted):
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
Before:
After:
Why are using setter functions faster than passing in values directly?
This is interesting. First of all, we need to focus on reacts way of updating state. In the documentation of react https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous There you can see:
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
For example, this code may fail to update the counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
A pretty good article on this is written by Jan Hesters here:
https://medium.com/#jan.hesters/updater-functions-in-reacts-setstate-63c7c162b16a
And more details here:
https://learn.co/lessons/react-updating-state
I'm trying to use react Hooks to update my react component when a change to the state holding the api information occurs. I'm currently creating a quiz application where the user can create a quiz , by entering data such as title and answers, which is then sent to a mongodb. When the user is not editing the quiz, I want to display the updated values from the database .
For example:
parent component
import React,{useState} from 'react';
export default function Context() {
const [data,setData] = useState(null);
const [selected,setSelected] = useState(id); // id is taken from url
useEffect(() => {
getKahootQuestion();
}, [data])
async function getKahootQuestion(slide) {
if(selected !== undefined) {
let getKahootQuestion = await withData(`quizQuestion/single/${selected}`,'GET');
if(getKahootQuestion) {
setData(getKahootQuestion.data);
console.log(getKahootQuestion);
}
}
}
}
export default function MainContent() {
// I want to be able to add items to Database and re-render components to reflect the changes
let context = useContext(Context);
let title = context.data[0].title;
async function submitTitle(e) {
e.preventDefault();
let form = e.currentTarget;
let title = form['title'].value;
setEditable(false);
let updateKahootQuestion = await withData(`quizQuestion/${context.selected}`,'PUT',{title});
}
return (
<>
<form>
<input name='title' />
</form>
<p>{title}</p>
</>
)
}
I am not sure if your intention is to do this strictly through the context, but we will need more of it there to know what properties and functions are exposed from it.
Regardless, to trigger a re-render on your MainContent component you could also simply add in a local useState and update as needed. It should look something like this:
export default function MainContent() {
let context = useContext(Context);
// Instead of just declaring a title variable, declare a title state using that same information.
const [title, setTitle] = useState(context.data[0].title)
async function submitTitle(e) {
e.preventDefault();
let form = e.currentTarget;
let title = form['title'].value;
setEditable(false);
let updateKahootQuestion = await withData(`quizQuestion/${context.selected}`,'PUT',{title});
// Not sure what your updateKahootQuestion will look like, so for the sake of this example, will just use title.
setTitle(title) // will trigger a re-render
}
return (
<>
<form>
<input name='title' />
</form>
<p>{title}</p>
</>
)
}
I am currently writing a map component using Mapbox. But I encounter an error on React hooks during development.
In useEffect state variable that is declared prints two different values.
As i explain in the below code. startDrawing is console.logs both true and false after second click on <IconButton/> button.
import React from "react";
import mapboxgl from "mapbox-gl";
import { Add, Delete } from "#material-ui/icons";
import IconButton from "#material-ui/core/IconButton";
export default function MapComponent() {
const mapContainerRef = React.useRef(null);
const [startDrawing, setStartDrawing] = React.useState(false);
const [map, setMap] = React.useState(null);
const initMap = () => {
mapboxgl.accessToken = "mapbox-token";
const mapbox = new mapboxgl.Map({
container: mapContainerRef.current,
style: "mapbox://styles/mapbox/streets-v11",
center: [0, 0],
zoom: 12,
});
setMap(mapbox);
};
React.useEffect(() => {
if (!map) {
initMap();
} else {
map.on("click", function (e) {
// After second click on set drawing mode buttons
// startDrawing value writes two values for each map click
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
console.log(startDrawing);
if (startDrawing) {
// do stuff
} else {
// do stuff
}
});
}
}, [map, startDrawing]);
return (
<>
<div>
{/* set drawing mode */}
<IconButton onClick={() => setStartDrawing(!startDrawing)}>
{startDrawing ? <Delete /> : <Add />}
</IconButton>
</div>
<div ref={mapContainerRef} />
</>
);
}
So my question is how can i solve this problem?
Thank you for your answers.
The issue is that you are adding a new event listener to map every time startDrawing changes. When you click on the rendered element all of those listeners are going to be fired, meaning you get every state of startDrawing the component has seen.
See this slightly more generic example of your code, and note that every time you click Add or Delete a new event listener gets added to the target element:
const { useState, useRef, useEffect } = React;
function App() {
const targetEl = useRef(null);
const [startDrawing, setStartDrawing] = useState(false);
const [map, setMap] = useState(null);
const initMap = () => {
setMap(targetEl.current);
};
useEffect(() => {
if (!map) {
initMap();
} else {
const log = () => console.log(startDrawing);
map.addEventListener('click', log);
}
}, [map, startDrawing]);
return (
<div>
<div>
<button onClick={() => setStartDrawing(!startDrawing)}>
{startDrawing ? <span>Delete</span> : <span>Add</span>}
</button>
</div>
<div ref={targetEl}>target element</div>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can fix this by adding a return statement to your useEffect. This is triggered immediately before the effect updates with new values from the dependency array, and also when the component unmounts. Inside the return statement you should remove the previous event listener so that only one is attached to the element at any given point. For the above example it would look like this:
useEffect(() => {
if (!map) {
initMap();
} else {
const log = () => console.log(startDrawing);
map.addEventListener('click', log);
return () => {
map.removeEventListener('click', log);
};
};
}, [map, startDrawing]);
Ideally you would not use the standard JS event syntax at all, as the convention in React is to attach events declaratively in the return/render function so that they can always reference the current state. However, you are using an external library, and I don't know whether it has any explicit support for React - you should probably check that out.
The issue is that useEffect will trigger both on mount and unmount (render and destroy). Refer to this documentation for a detailed explanation.
To run the function only on the first render, you can pass an empty array as the second parameter of useEffect, just like this:
useEffect(()=>{
//do stuff
},[]); // <--- Look at this parameter
The last parameter serves as a flag and usually a state should be passed, which will make useEffect's function trigger only if the parameter's value is different from the previous.
Let's assume you want to trigger useEffect each and every time your state.map changes - you cold do the following:
const [map, setMap] = React.useState(null);
useEffect(()=>{
//do stuff
},map); // if map is not different from previous value, function won't trigger
I have this code.
and here is the code snippet
const [indicators, setIndicators] = useState([]);
const [curText, setCurText] = useState('');
const refIndicator = useRef()
useEffect(() => {
console.log(indicators)
}, [indicators]);
const onSubmit = (e) => {
e.preventDefault();
setIndicators([...indicators, curText]);
setCurText('')
}
const onChange = (e) => {
setCurText(e.target.value);
}
const MemoInput = memo((props)=>{
console.log(props)
return(
<ShowIndicator name={props.name}/>
)
},(prev, next) => console.log('prev',prev, next)
);
It shows every indicator every time I add in the form.
The problem is that ShowIndicator updates every time I add something.
Is there a way for me to limit the the time my App renders because for example I created 3 ShowIndicators, then it will also render 3 times which I think very costly in the long run.
I'm also thinking of using useRef just to not make my App renders every time I input new text, but I'm not sure if it's the right implementation because most documentations recommend using controlled components by using state as handler of current value.
Observing the given sandbox app behaviour, it seems like the whole app renders for n times when there are n indicators.
I forked the sandbox and moved the list to another functional component (and memo'ed it based on prev and next props.
This will ensure my 'List' is rendered every time a new indicator is added.
The whole app will render only when a new indicator is added to the list.
Checkout this sandbox forked from yours - https://codesandbox.io/embed/avoid-re-renders-react-l4rm2
React.memo will stop your child component rendering if the parent rerenders (and if the props are the same), but it isn't helping in your case because you have defined the component inside your App component. Each time App renders, you're creating a new reference of MemoInput.
Updated example: https://codesandbox.io/s/currying-tdd-mikck
Link to Sandbox:
https://codesandbox.io/s/musing-kapitsa-n8gtj
App.js
// const MemoInput = memo(
// props => {
// console.log(props);
// return <ShowIndicator name={props.name} />;
// },
// (prev, next) => console.log("prev", prev, next)
// );
const renderList = () => {
return indicators.map((data,index) => {
return <ShowIndicator key={index} name={data} />;
});
};
ShowIndicator.js
import React from "react";
const ShowIndicator = ({ name }) => {
console.log("rendering showIndicator");
const renderDatas = () => {
return <div key={name}>{name}</div>;
};
return <>{renderDatas()}</>;
};
export default React.memo(ShowIndicator); // EXPORT React.memo