Unsure how to unmount scene in #react/three-fiber - javascript

I'm building this scene, and I have a few issues.
One small issue is with directionalLight. When I try using more up to date three.js attributes like shadow.camera.far on it, #react-three/fiber loses the ability to call the dispose function on the light's shadows. So, I have to use the deprecated properties, which causes me to get these annoying warnings. That's not the biggest problem though. I can live with that, but I'd also be grateful if anyone had any idea how to get rid of those warnings.
The main problem is that when I try making the model in my scene clickable so that the user can go to a different page in the app, I get a #react-three/fiber error of Uncaught TypeError: Cannot read property 'disconnect' of undefined. I think this is because I'm using the unmountComponentAtNode function in the scene's useEffect hook, and it's causing the component to unmount before #react-three/fiber can properly remove all the threejs objects. However, when I remove that function, I get the common react error of: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Anyone have any idea what I can do? I'd appreciate any help.
Here's my code:
import React, { Suspense, useRef, useEffect, useState } from 'react'
import {
Canvas,
extend,
unmountComponentAtNode,
useFrame,
useLoader,
useThree,
} from '#react-three/fiber'
import { CubeTextureLoader, DoubleSide, PerspectiveCamera, sRGBEncoding } from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import Model3D from '../../assets/models/model.glb'
import './Model.css'
import nx from '../../assets/environmentMap/nx.jpg'
import ny from '../../assets/environmentMap/ny.jpg'
import nz from '../../assets/environmentMap/nz.jpg'
import px from '../../assets/environmentMap/px.jpg'
import py from '../../assets/environmentMap/py.jpg'
import pz from '../../assets/environmentMap/pz.jpg'
extend({ OrbitControls })
const wallMap = {
'side-panel': '/wall/1',
'panel-material': '/wall/2',
'panel-material-2': '/wall/3',
'side-panel-2': '/wall/4',
entrance: '/wall/5',
'overhead-entrance': '/wall/5',
}
const CameraControls = () => {
const {
camera,
gl: { domElement },
} = useThree()
const controls = useRef()
useFrame(() => controls.current.update())
return <orbitControls ref={controls} args={[camera, domElement]} enableDamping />
}
const SkyBox = () => {
const { scene } = useThree()
const loader = new CubeTextureLoader()
const environmentMap = loader.load([px, nx, py, ny, pz, nz])
environmentMap.encoding = sRGBEncoding
scene.background = environmentMap
scene.environment = environmentMap
return null
}
const Model = ({ changePage }) => {
const active = useRef(false)
const moving = useRef(false)
const material = useRef()
const { scene } = useThree()
const gltf = useLoader(GLTFLoader, Model3D)
const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(-4.62, 1.3, 4)
scene.add(camera)
const handleClick = (e) => {
if (moving.current) return
const {
material: { name },
} = e.intersections[0].object
changePage(wallMap[name])
}
const handlePointerDown = () => {
active.current = true
}
const handlePointerUp = () => {
if (active.current) {
setTimeout(() => {
moving.current = false
active.current = false
}, 30)
}
}
const handlePointerMove = () => {
if (active.current) {
moving.current = true
}
}
return (
<primitive
object={gltf.scene}
position={[0, -1.25, 0]}
scale={0.22}
rotation={[0, -0.6, 0]}
castShadow
onClick={handleClick}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
<meshStandardMaterial attach='material' ref={material} />
</primitive>
)
}
const Plane = () => {
return (
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.8, 0]}>
<planeGeometry args={[12.5, 12.5, 2, 2]} />
<meshStandardMaterial color='#222' side={DoubleSide} />
</mesh>
)
}
const Scene = () => {
const [redirectPath, setRedirectPath] = useState('')
useEffect(() => {
if (redirectPath) window.appHistory.push(redirectPath)
return () => {
unmountComponentAtNode(document.querySelector('canvas'))
}
}, [redirectPath])
return (
<div className='webgl'>
<Canvas>
<Suspense fallback={null}>
<ambientLight color='#ffffff' intensity={0.3} />
<directionalLight
position={[-3, 5, 5]}
color='#ffffff'
intensity={3}
castShadow
// Last 4 are deprecated properties that cause warnings from three.js.
// When I use something like shadow={ { bias: 0.05 } }, I lose the dispose
// function from the shadow. So, I'm still using them for now.
shadowBias={0.05}
shadowCameraFar={15}
shadowMapHeight={1024}
shadowMapWidth={1024}
/>
<CameraControls />
<Model changePage={(x) => setRedirectPath(x)} />
<Plane />
<SkyBox />
</Suspense>
</Canvas>
</div>
)
}
export default Scene

Related

React - generating a unique random key causes infinite loop

I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}

Access to other components variables in react

I'm using react three fiber and i have two components
one to make a box and the other is to make an array of this box
here's how they look like, the platform component:
export function Plat() {
const [active, setActive] = useState(0)
const { scale } = useSpring({ scale: active ? 1 : 0.4 })
const [mat] = useState(() => new THREE.TextureLoader().load('/matcap.png'))
function Shape(props) {
return (
<animated.mesh {...props} scale={scale} onPointerOver={() => setActive(Number(!active))} >
<RoundedBox args={[60,20,60]} radius={4} smoothness={4} position={[0, -10, 0]} dispose={null} >
<meshMatcapMaterial matcap={mat} />
</RoundedBox>
</animated.mesh>
);
}
useEffect(() => {
(function() {
setActive((1))
})();
},[]);
return (
<Shape />
)
}
and the component that makes an array of this platform:
import {Plat} from './Plat'
export default function Boxes() {
function MyShape(props) {
return (
<mesh {...props} >
<Plat />
</mesh>
);
}
const [shapes, setShapes] = useState([<MyShape key={0} position={[100, 100, 100]} />, <MyShape key={1} position={[120, 120, 120]} />, <MyShape key={2} position={[130, 130, 130]} />]);
return (
<group >
{[...shapes]}
</group>
)
}
(I have more than 3 elements in the array)
I wanna know if there's a way to access the variables inside each of the array's platform components
how would I do something like this:
console.log(shapes[2].position) or change the position of this specific shape in the array
or this
shapes[1].setActive(1)
is it even possible?
Few pointer
Avoid nesting functional components
Plat
Shape // declare this outside and pass it the params from parent
If you need to change the position of the children, consider creating a array with the information you need to change
const [shapes, setShapes] = useState<>([{ key: 1, position: { x: 5, y: 10 } }])
const changeShapePostions = (key) => {
setShapes(items => {
return items.map(shape => {
if (shape.id === key) {
return { ...shape, position: { x: updatedX, y: updated: y} }
} else {
return shape
}
}
}
}
const setActive = (key) => {
setShapes(items => {
return items.map(shape => {
if (shape.id === key) {
return { ...shape, isActive: true }
} else {
return shape
}
}
}
}
return (
<group >
{
shapes.map(shape => {
return (<MyShape key={shape.key} position={shape.position} setActive={setActive} />)
}
}
</group>
)
you can check standard practices on working with arrays
Hope it helps in some ways

How to animate mapped elements one at a time in react-native?

I mapped an object array to create a tag element with the details being mapped onto the element. And then I created an animation so on render, the tags zoom in to full scale. However, I was wanting to take it to the next step and wanted to animate each tag individually, so that each tag is animated in order one after the other. To me, this seems like a common use of animations, so how could I do it from my example? Is there any common way to do this that I am missing?
import {LeftIconsRightText} from '#atoms/LeftIconsRightText';
import {LeftTextRightCircle} from '#atoms/LeftTextRightCircle';
import {Text, TextTypes} from '#atoms/Text';
import VectorIcon, {vectorIconTypes} from '#atoms/VectorIcon';
import styled from '#styled-components';
import * as React from 'react';
import {useEffect, useRef} from 'react';
import {Animated, ScrollView} from 'react-native';
export interface ICustomerFeedbackCard {
title: string;
titleIconName: string[];
tagInfo?: {feedback: string; rating: number}[];
}
export const CustomerFeedbackCard: React.FC<ICustomerFeedbackCard> = ({
title,
titleIconName,
tagInfo,
...props
}) => {
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
const zoomAnim = useRef(new Animated.Value(START_ZOOM_SCALE)).current;
/**
* Creates an animation with a
* set duration and scales the
* size by a set factor to create
* a small zoom effect
*/
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: FINAL_ZOOM_SCALE,
duration: FAST_ZOOM,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
/**
* Sorts all tags from highest
* to lowest rating numbers
* #returns void
*/
const sortTags = () => {
tagInfo?.sort((a, b) => b.rating - a.rating);
};
/**
* Displays the all the created tags with
* the feedback text and rating number
* #returns JSX.Element
*/
const displayTags = () =>
tagInfo?.map((tag) => (
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
return (
<CardContainer {...props}>
<HeaderContainer>
<LeftIconsRightText icons={titleIconName} textDescription={title} />
<Icon name="chevron-right" type={vectorIconTypes.SMALL} />
</HeaderContainer>
<ScrollOutline>
<ScrollContainer>
{sortTags()}
{displayTags()}
</ScrollContainer>
</ScrollOutline>
<FooterContainer>
<TextFooter>Most recent customer compliments</TextFooter>
</FooterContainer>
</CardContainer>
);
};
And here is the object array for reference:
export const FEEDBACKS = [
{feedback: 'Good Service', rating: 5},
{feedback: 'Friendly', rating: 2},
{feedback: 'Very Polite', rating: 2},
{feedback: 'Above & Beyond', rating: 1},
{feedback: 'Followed Instructions', rating: 1},
{feedback: 'Speedy Service', rating: 3},
{feedback: 'Clean', rating: 4},
{feedback: 'Accommodating', rating: 0},
{feedback: 'Enjoyable Experience', rating: 10},
{feedback: 'Great', rating: 8},
];
Edit: I solved it by replacing React-Native-Animated and using an Animated View and instead using Animatable and using an Animatable which has built in delay. Final solution:
const displayTags = () =>
tagInfo?.map((tag, index) => (
<TagContainer animation="zoomIn" duration={1000} delay={index * 1000}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
Here is a gif of the animation
This is an interesting problem. A clean way you could approach this problem is to develop a wrapper component, DelayedZoom that will render its child component with a delayed zoom. This component would take a delay prop that you can control to add a delay for when the component should begin animation.
function DelayedZoom({delay, speed, endScale, startScale, children}) {
const zoomAnim = useRef(new Animated.Value(startScale)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
delay: delay,
toValue: endScale,
duration: speed,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
return (
<Animated.View
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
{children}
</Animated.View>
);
}
After this, you can use this component as follows:
function OtherScreen() {
const tags = FEEDBACKS;
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
function renderTags() {
return tags.map((tag, idx) => {
const delay = idx * 10; // play around with this. Main thing is that you get a sense for when something should start to animate based on its index, idx.
return (
<DelayedZoom
delay={delay}
endScale={FINAL_ZOOM_SCALE}
startScale={START_ZOOM_SCALE}
speed={FAST_ZOOM}>
{/** whatever you want to render with a delayed zoom would go here. In your case it may be TagContainer */}
<TagContainer>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</DelayedZoom>
);
});
}
return <View>{renderTags()}</View>;
}
I hope this helps to point you in the right direction!
Also some helpful resources:
Animation delays: https://animationbook.codedaily.io/animated-delay/
Demo
It is a bit of work to implement this, I didn't have your components to try it out so I have created a basic implementation, I hope this will help
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View, Animated } from "react-native";
const OBJ = [{ id: 1 }, { id: 2 }, { id: 3 }];
const Item = ({ data, addValue }) => {
const zoomAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start(() => {
addValue();
});
};
zoomIn();
}, [zoomAnim]);
return (
<View>
<Animated.View
ref={zoomAnim}
style={[
{
transform: [{ scale: zoomAnim }]
}
]}
>
<Text style={styles.text}>{data}</Text>
</Animated.View>
</View>
);
};
function App() {
const [state, setState] = useState([OBJ[0]]);
const addValue = () => {
const currentId = state[state.length - 1].id;
if (OBJ[currentId]) {
const temp = [...state];
temp.push(OBJ[currentId]);
setState(temp);
}
};
return (
<View style={styles.app}>
{state.map((item) => {
return <Item data={item.id} key={item.id} addValue={addValue} />;
})}
</View>
);
}
const styles = StyleSheet.create({
text: {
fontSize: 20
}
});
export default App;
Basically, I am adding an element to the state at the end of the previous animation, one thing to note is that the key is very important, and don't use the index as a key. instead of Ids you might want to add any other value that is sorted or maybe link an item by passing the id of the previous item.
ADDING A SOLUTION USING REANIMATED AND MOTI
There is this library which you can use moti(https://moti.fyi/) it will work with reanimated, so you need to add reanimated too. Before using Reanimated you must consider that your normal chrome dev tools for that particular application will stop working with reanimated 2.0 and above you can use flipper though.
coming to the solution.
import { View as MotiView } from 'moti';
...
const displayTags = () =>
tagInfo?.map((tag, index) => (
<MotiView
key = {tag.id}
from={{ translateY: 20, opacity: 0 }}
animate={{ translateY: 0, opacity: 1 }}
transition={{ type: 'timing' }}
duration={500}
delay={index * 150}>
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</MotiView>
));
...
That's it, make sure to use a proper key, don't use index as key.
Side Note: If you are doubtful that sould you use reanimated or not, just go through https://docs.swmansion.com/react-native-reanimated/docs/ this page. Using Moti you can have really cool animation easily also if you reanimated version 2.3.0-alpha.1 then you need not to use Moti but as it is alpha version so it is not advisable to use in production you can wait for its stable release too.

How to find out what can be exported from npm package

I'm trying to replicate this https://codepen.io/swizec/pen/bgvEvp
I've installed the d3-timer package with npm https://www.npmjs.com/package/d3-timer
It is definitely there because I have read through the files.
What I am confused about is how to import the timer into my code. In the code on codepen it just uses d3.timer but doesn't show the import above. So I tried importing d3 but it can't find it in the d3-timer package. I tried timer, Timer, D3, d3.
So my question is - how do I go about investigating the package to work out what the names of the exports are?
Or if that is too complicated - in this particular case what should I be importing to get the functionality of d3.timer?
Many thanks!
Code from code pen:
const Component = React.Component;
const Ball = ({ x, y }) => (
<circle cx={x} cy={y} r={5} />
);
const MAX_H = 750;
class App extends Component {
constructor() {
super();
this.state = {
y: 5,
vy: 0
}
}
componentDidMount() {
this.timer = d3.timer(() => this.gameLoop());
this.gameLoop();
}
componentWillUnmount() {
this.timer.stop();
}
gameLoop() {
let { y, vy } = this.state;
if (y > MAX_H) {
vy = -vy*.87;
}
this.setState({
y: y+vy,
vy: vy+0.3
})
}
render() {
return (
<svg width="100%" height={MAX_H}>
<Ball x={50} y={this.state.y} />
</svg>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'));
my code
import React from 'react';
import d3 from 'd3-timer'
const Component = React.Component;
const Ball = ({ x, y }) => (
<circle cx={x} cy={y} r={5} />
);
const MAX_H = 750;
export default class App extends Component {
constructor() {
super();
this.state = {
y: 5,
vy: 0
}
}
componentDidMount() {
this.timer = d3.timer(() => this.gameLoop());
this.gameLoop();
}
componentWillUnmount() {
this.timer.stop();
}
gameLoop() {
let { y, vy } = this.state;
if (y > MAX_H) {
vy = -vy*.87;
}
this.setState({
y: y+vy,
vy: vy+0.3
})
}
render() {
return (
<svg width="100%" height={MAX_H}>
<Ball x={50} y={this.state.y} />
</svg>
)
}
}
Error message:
Attempted import error: 'd3-timer' does not contain a default export (imported as 'd3').
Try
import { timer } from 'd3-timer' and then use timer()

setting state with onChange in React results in canvas being null

I have a simple application consisting of two - parent App and child Circle components. My aim is to draw circle on canvas (Circle component does that) and have input that takes number value and makes angle of that numerical value with the radius. like so :
example
The problem is, i've set up onChange event listener with handler on input, which is supposed to update my state, but whenever i type something into input field, it tells me that my canvas is null, and .getContext("2d") method can't be performed, which messes up entire application.
I've googled and found out that this is because of the fact that canvas renders after state is updated, or something like that. I can't think of any solution that would make that possible. here's my code :
App.js
import React, { Component } from "react";
import "./App.css";
import { degreesToRadiansFlipped } from "./helpers/helpers";
import Circle from "./components/Circle";
class App extends Component {
state = { degrees: 0 };
handleChange = (event) => {
this.setState({ degrees: event.target.value });
};
coordinates = {
x: Math.cos(degreesToRadiansFlipped(120)) * 100 + 150,
y: Math.sin(degreesToRadiansFlipped(120)) * 100 + 150
};
drawCircle = (context, x, y) => {
context.beginPath();
context.arc(150, 150, 100, 0, Math.PI * 2);
context.moveTo(150, 150);
context.lineTo(x, y);
context.stroke();
};
render() {
console.log(this.currentDegreeValue);
return (
<div className="main">
<Circle drawCircle={this.drawCircle} coordinates={this.coordinates} />
<form>
<input name="degrees" type="text" onChange={this.handleChange} />
</form>
</div>
);
}
}
export default App;
Circle.js
import React, { Component } from "react";
class Circle extends Component {
componentDidMount() {
this.props.drawCircle(
this.context,
this.props.coordinates.x,
this.props.coordinates.y
);
}
render() {
return (
<canvas
ref={(canvas) => (this.context = canvas.getContext("2d"))}
width={300}
height={300}
/>
);
}
}
export default Circle;
Actual Error is :
TypeError: Cannot read property 'getContext' of null
// index.js
import React, { Component } from "react";
import ReactDOM from "react-dom";
import Circle from "./Circle";
class App extends Component {
state = { degrees: 0, showCirle: false };
handleChange = event => {
this.setState({ degrees: event.target.value, showCirle: true });
};
degreesToRadiansFlipped(angle) {
return (angle + Math.PI) % (2 * Math.PI);
}
coordinates = {
x: Math.cos(this.degreesToRadiansFlipped(120)) * 100 + 150,
y: Math.sin(this.degreesToRadiansFlipped(120)) * 100 + 150
};
drawCircle = (context, x, y) => {
console.log("inside drawcircle");
context.beginPath();
context.arc(150, 150, 100, 0, Math.PI * 2);
context.moveTo(150, 150);
context.lineTo(x, y);
context.stroke();
};
render() {
console.log(this.currentDegreeValue);
return (
<div className="main">
{this.state.showCirle && (
<Circle drawCircle={this.drawCircle} coordinates={this.coordinates} />
)}
<form>
<input name="degrees" type="text" onChange={this.handleChange} />
</form>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// Circel Component
import React, { Component } from "react";
class Circle extends Component {
constructor(props) {
super(props);
}
fillContext = () => {
const canvas = this.refs.canvas
const ctx = canvas.getContext("2d")
this.context = ctx
this.props.drawCircle(
this.context,
this.props.coordinates.x,
this.props.coordinates.y
);
}
componentDidMount() {
this.fillContext()
}
componentDidUpdate() {
this.fillContext()
}
render() {
return <canvas ref="canvas" width={300} height={300} />;
}
}
export default Circle;
In your example, x and y coordinates should change based on input value change, but coordinates is an object with fixed x and y values. I think that logic should also change.

Categories

Resources