Using react-native, I'm creating sub-Components within the parent App and providing their position to the array this.state.objLocation within the parent App.
I can get the initial location data into the array straight after the render, but because my subcomponents are draggable, each time they re-render on drag, it adds a new position object to the array.
I'd like to avoid this, and I thought that creating this.state = { firstRender: true } in the constructor and then using componentDidMount = () => { this.setState({ firstRender: false }) } after the first render would allow me to create a 'gate' to stop the addition of the extra position objects.
I can see that if I comment out //componentDidMount = () => { this.setState({ firstRender: false }) } then I will get multiple entries to my array but if it's included in the class I get absolutely none.
So possibly my interpretation of the render lifecycle and componentDidMount is incorrect?
Here is my code.
// App
import React, { Component } from 'react';
import { View, Text, } from 'react-native';
import styles from './cust/styles';
import Draggable from './cust/draggable';
const dataArray = [{num: 1,id: 'A',},{num: 2,id: 'B',},{num: 3,id: 'Z',}]
export default class Viewport extends Component {
constructor(props){
super(props);
this.state = {
dID : null,
objLocation: [],
firstRender: true,
};
}
render(){
return (
<View style={styles.mainContainer}>
<View style={styles.draggableContainer}>
<Text>Draggable Container</Text> {dataArray.map( d => { return(
<Draggable
id={d.id}
onLayout={ e=> this.onLayout(e)}
onPanResponderGrant={(dID) =>this.setState({ dID })}
onPanResponderRelease={() => this.setState({dID: null})} /> ) })}
<View style={[styles.findPoint ]} />
</View>
<View style={styles.infoBar}>
<Text>{this.state.dID ? this.state.dID : ''}</Text>{this.compFrame()}
</View>
</View>
);
}
onLayout = (e) => {
if ( e && this.state.firstRender) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
objLocation: this.state.objLocation.concat([position])
});
}
}
componentWillMount = () => {
console.log("START");
}
compFrame = () => {
return(
this.state.objLocation.map( d => {<View style={[styles.findPoint2,{left: d.x, top: d.y, width: d.width, height: d.height} ]} ></View>})
)
}
componentDidMount = () => {
this.setState({firstRender: true })
console.log(this.state.objLocation.length);
}
}
// Draggable
import React, { Component } from 'react';
import { Text, PanResponder, Animated } from 'react-native';
import styles from './styles';
class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.props.onPanResponderGrant(this.props.id);
},
onPanResponderMove: Animated.event([ null, {
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(this.state.pan, { toValue: { x: 0, y: 0 } }).start();
this.props.onPanResponderRelease();
},
});
}
render() {
return (
<Animated.View
onLayout={ (e) => this.props.onLayout(e) }
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circleAlt, styles.position]}>
<Text style={styles.textAlt}>Drag me!</Text>
<Text style={styles.textNum}>{this.props.id}</Text>
</Animated.View>
);
}
componentDidMount = () => {
this.props.onLayout(this.props.dragEvent)
}
}
export default Draggable;
// Output of console.log
START xxx
0
{width:108,height:108,x:133.5,y:376.5}
{width:108,height:108,x:133.5,y:78.5}
{width:108,height:108,x:133.5,y:227.5}
You could set the firstRender state in onLayout function
onLayout = (e) => {
if ( e && this.state.firstRender) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
firstRender: false,
objLocation: this.state.objLocation.concat([position])
});
}
}
According to the information provided by you, your onLayout function is called by the component so its not included in the component lifecycle process, so when the component completes its lifecycle it goes into componentDidMount after mounting (which is not calling onLayout func) & thus changed the firstRender state to false and hence when you drag the component each time it goes from true to false.
I hope this explains
I feel like I've hacked this, to get it to work, so please correct me as to correct procedure.
This is the onLayout method from the App. I've included an if statement that checks if the new positions array length is equal too the dataArray length that the draggable items are based on.
It looks like this.
onLayout = (e) => {
if ( this.state.objLocation.length != dataArray.length ) {
if ( e ) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
objLocation: this.state.objLocation.concat([position])
});
}
}
}
Related
I'm creating a custom mouse cursor per component (e.g. a custom mouse cursor on a figure element). I'm writing a custom hook for this. This is the hook so far:
const useMouseCoords = () => {
let [coords, setCoords] = useState({ x: 0, y: 0 })
// I need to calculate the coordinates from the parents offset. Here is where I'm stuck.
let offsetParent = parentRef.current.getBoundingClientRect()
function handleCoords(e) {
setCoords({
x: e.clientX - offsetParent.x,
y: e.clientY - offsetParent.y,
})
}
useEffect(() => {
if (typeof window === `undefined`) return // escape gatsby build process
window.addEventListener('mousemove', handleCoords)
return () => {
window.removeEventListener('mousemove', handleCoords)
}
}, [])
return coords
}
The mousecursor component is quite simple:
const MouseCursor = (props) => {
let { x, y } = useMouseCoords()
return (
<div
className="mouse-cursor-button"
style={{
transform: `translate(${x}px, ${y}px)`,
}}
>
<div className="mouse-cursor-button__text">Click to play</div>
</div>
)
}
Code of course doesn't work but is rather to illustrate what I'm trying to achieve.
So I need the parent of the MouseCursor component to calculate the offset. I'm stuck at the part where I want to reference the parent component. I was hoping I could pass it as an argument to the hook.
So the question is how can I access the parent component in the hook?
Can you not just pass the ref down like:
function Parent() {
const ref = useRef()
return <div ref={ref}><MouseCursor parentRef={ref} /></div>
}
const MouseCursor = (props) => {
let { x, y } = useMouseCoords(props.parentRef)
return (
<div
className="mouse-cursor-button"
style={{
transform: `translate(${x}px, ${y}px)`,
}}
>
<div className="mouse-cursor-button__text">Click to play</div>
</div>
)
}
See https://codesandbox.io/s/0y46034oyl?fontsize=14 for example
You can have a function in the parent component that interacts with it's state and then the uses its state in the props of the children.
class Parent extends Component{
constructor(props){
super(props);
this.state = {
x: 0,
y:0
};
}
//...
function setCoords(x, y){
this.setState({
x: x,
y: y
})
}
//...
render(){
return(
<Child
actions={ {setParentCoords: this.setCoords.bind(this)} }
x={ this.state.x }
y={ this.state.y }
)
}
Now in your child you have access to props.actions.setParentCoords()
I've playing around with animation implemented with reactjs.
In the app I created a car which drives around a track. On this track there are obstacles, which the car should recognize.
I'm using window.setInterval for the repeating events. Maybe this is not the best option, but actually I don't know how to do else.
Since some changes, there are multiple intervals running.
But actually I don't know the reason for it. Can anybody give me a hint, why the racer component is instantly running in componentdidmount event?
The Racer component is giving the current position and degree / ankle to the Track component. The Track component is storing these values in states and giving it to the Racer component as props. But this should not lead to instantly firing componentdidmount event of Racer component, or?
Here is my code:
App.js
import React, { Component } from 'react';
import Track from './components/track.js';
const uuidv1 = require('uuid/v1');
class App extends Component {
constructor(props) {
super(props);
this.state = {
obstacles: [
{
key: uuidv1(),
position: {
left: 500,
top:10,
},
width: 25,
height: 25,
},
{
key: uuidv1(),
position: {
left: 650,
top:60,
},
width: 25,
height: 25,
}
],
};
}
render() {
return (
<div className="App">
<Track width={800} height={100} obstacles={this.state.obstacles}>
</Track>
</div>
);
}
}
export default App;
Track.js
import React, { Component } from 'react';
import styled from 'styled-components';
import Racer from './racer.js';
import Obstacle from './obstacle';
import centralStrip from '../images/centralStrip.png';
const uuidv1 = require('uuid/v1');
class Track extends Component {
constructor(props) {
super(props);
this.state = {
racerCurrentPosition: {
top: 60,
left:150
},
racerDegree: 0,
};
}
componentDidMount() {
}
handleObstacleCheck(position, racerPosition) {
let obstacleFound = false;
obstacleFound = this.props.obstacles.map((obstacle) => {
let returnValue = false;
let obstacleRect = document.getElementById(obstacle.key).getBoundingClientRect();
if( position.right >= obstacleRect.left && position.right <= obstacleRect.right && racerPosition.top >= obstacleRect.top && racerPosition.bottom <= obstacleRect.bottom) {
returnValue = true;
}
return returnValue;
});
let isObstacleFound = false;
if(obstacleFound.indexOf(true) !== -1) {
isObstacleFound = true;
}
return isObstacleFound;
}
handleRacerPositionChange(position) {
this.setState({
racerCurrentPosition: position,
});
}
handleRacerDegreeChange(newDegree) {
this.setState({
racerDegree: newDegree,
});
}
render() {
return (
<TrackImage key={uuidv1()}
id="track"
width={this.props.width}
height={this.props.height}>
<Racer key={uuidv1()}
position={this.state.racerCurrentPosition}
onRacerPositionChange={this.handleRacerPositionChange.bind(this)}
degree={this.state.racerDegree}
onRacerDegreeChange={this.handleRacerDegreeChange.bind(this)}
obstacleFound={this.state.obstacleFound}
trackWidth={this.props.width}
trackHeight={this.props.height}
onObstacleCheck={this.handleObstacleCheck.bind(this)}
/>
{
this.props.obstacles.map((obstacle) => {
return (
<Obstacle key={obstacle.key}
id={obstacle.key}
position={obstacle.position}
width={obstacle.width}
height={obstacle.height}
/>
);
})
}
</TrackImage>
);
}
}
export default Track;
Racer.js
import React, { Component, Fragment } from 'react';
import styled from 'styled-components';
import HelperDistance from './helpers/distance.js';
import HelperCenterCar from './helpers/centerCar.js';
import racerImage from '../images/racer.png';
const uuidv1 = require('uuid/v1');
class Racer extends Component {
constructor(props) {
super(props);
this.state = {
key: uuidv1(),
intervalId: 0,
speed: 0,
helperForLeftPositioning: 0,
helperForTopPositioning: 0,
isMoving: false,
collision: false,
centerOfCarCoordinates: {
x: 25,
y: 12.5
},
obstacleFound: false,
};
this.start = this.start.bind(this);
this.move = this.move.bind(this);
}
componentDidMount() {
if(this.state.intervalId === 0) {
this.start();
}
}
componentWillUnmount() {
window.clearInterval(this.state.intervalId);
}
start() {
this.setState({
speed: 3,
isMoving: true,
}, () => {
this.createInterval();
});
}
stop() {
this.setState({
speed: 0,
isMoving: false,
}, () => {
window.clearInterval(this.state.intervalId);
});
}
move() {
if(this.state.obstacleFound === true) {
let newDegree;
if(this.props.degree === 0) {
newDegree = 360;
}
newDegree--;
this.props.onRacerDegreeChange(newDegree);
}
this.step();
}
step() {
if(this.state.isMoving) {
//...calculate new position
this.setState({
helperForTopPositioning: helperForTopPositioning,
helperForLeftPositioning: helperForLeftPositioning,
},() => {
let position = {
left: positionNewLeft,
top: positionNewTop
};
this.props.onRacerPositionChange(position);
});
}
}
createInterval = () => {
let intervalId = window.setInterval(() => {
this.move();
console.log("IntervalId: " + intervalId);
},100);
this.setState({
intervalId: intervalId,
})
}
handleDistanceChange(position) {
let racerRect = document.getElementById(this.state.key).getBoundingClientRect();
let obstacleFound = this.props.onObstacleCheck(position, racerRect);
if(this.state.obstacleFound !== obstacleFound) {
this.setState({
obstacleFound: obstacleFound
});
}
}
render() {
return (
<Fragment>
<Car key={this.state.key} id={this.state.key} position={this.props.position} degree={this.props.degree}>
<HelperCenterCar key={uuidv1()} position={this.state.centerOfCarCoordinates} degree={this.props.degree} />
<HelperDistance key={uuidv1()} onChange={this.handleDistanceChange.bind(this)} position={this.state.centerOfCarCoordinates} degree={this.props.degree} />
</Car>
</Fragment>
);
}
}
export default Racer;
The HelperCenterCar and HelperDistance are components, which helps to identify, if there is an obstacle in the way. I'll post just the code of HelperDistance, because here instantly state updates are fired.
HelperDistance.js
import React, { Component } from 'react';
import styled from 'styled-components';
const uuidv1 = require('uuid/v1');
class HelperDistance extends Component {
constructor(props) {
super(props);
this.state = {
key: uuidv1(),
};
}
componentDidMount() {
this.handleOnChange();
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.handleOnChange();
}
handleOnChange() {
let position = document.getElementById(this.state.key).getBoundingClientRect();
this.props.onChange(position);
}
render() {
return (
<Line id={this.state.key} key={this.state.key} position={this.props.position} degree={this.props.degree} />
);
}
}
export default HelperDistance;
I'm encountering this strange issue that I can figure out why is happing.
This should not be happening since the prop passed down to the History component has not been updated.
./components/History.js
...
const History = ({ previousLevels }) => {
return (
<ScrollView style={styles.container}>
{previousLevels.reverse().map(({ date, stressValue, tirednessValue }) => {
return (
<CardKBT
key={date}
date={date}
stressValue={stressValue}
tirednessValue={tirednessValue}
/>
)
})}
</ScrollView>
)
}
...
export default History
As can be seen in this code (below), the prop to the History is only updated once the user press Save.
App.js
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { AppLoading, Font } from 'expo'
import Store from 'react-native-simple-store'
import { debounce } from 'lodash'
import CurrentLevels from './components/CurrentLevels'
import History from './components/History'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false,
currentLevels: {
stressValue: 1,
tirednessValue: 1,
},
previousLevels: [],
}
this.debounceUpdateStressValue = debounce(this.onChangeStressValue, 50)
this.debounceUpdateTirednessValue = debounce(
this.onChangeTirednessValue,
50
)
}
async componentDidMount() {
const previousLevels = await Store.get('previousLevels')
if (previousLevels) {
this.setState({ previousLevels })
}
}
render() {
const { stressValue, tirednessValue } = this.state.currentLevels
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
...
/>
)
} else {
return (
<View style={{ flex: 1 }}>
<CurrentLevels
stressValue={stressValue}
onChangeStressValue={this.debounceUpdateStressValue}
tirednessValue={tirednessValue}
onChangeTirednessValue={this.debounceUpdateTirednessValue}
onSave={this.onSave}
/>
<History previousLevels={this.state.previousLevels} />
</View>
)
}
}
...
onChangeStressValue = stressValue => {
const { tirednessValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onChangeTirednessValue = tirednessValue => {
const { stressValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onSave = () => {
Store.push('previousLevels', {
date: `${new Date()}`,
...this.state.currentLevels,
}).then(() => {
Store.get('previousLevels').then(previousLevels => {
this.setState({
currentLevels: { stressValue: 1, tirednessValue: 1 },
previousLevels,
})
})
})
}
}
The component will re-render when one of the props or state changes, try using PureComponent or implement shouldComponentUpdate() and handle decide when to re-render.
Keep in mind, PureComponent does shallow object comparison, which means, if your props have nested object structure. It won't work as expected. So your component will re-render if the nested property changes.
In that case, you can have a normal Component and implement the shouldComponentUpdate() where you can tell React to re-render based on comparing the nested properties changes.
I am running the following code in Android emulator but I am getting null is not an object (evaluating 'this.state.dataSource') error.
Please, could you help me to see what I am doing wrong? For some reason the line dataSource={this.state.dataSource} is getting null.
import React, {
Component
} from 'react';
import {
AppRegistry,
ActivityIndicator,
ListView,
Text,
View,
StyleSheet
} from 'react-native';
import Row from './Row';
import Header from './Header';
import SectionHeader from './SectionHeader';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
separator: {
flex: 1,
height: StyleSheet.hairlineWidth,
backgroundColor: '#8E8E8E',
},
});
export default class NoTocarList extends Component {
constructor(props) {
super(props);
const getSectionData = (dataBlob, sectionId) => dataBlob[sectionId];
const getRowData = (dataBlob, sectionId, rowId) =>
dataBlob[`${rowId}`];
fetch('http://xxxxx.mybluemix.net/get')
.then((response) => response.json())
.then((responseJson) => {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
getSectionData,
getRowData
});
const {
dataBlob,
sectionIds,
rowIds
} =
this.formatData(responseJson);
this.state = {
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
}
})
}
formatData(data) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const dataBlob = {};
const sectionIds = [];
const rowIds = [];
for (let sectionId = 0; sectionId < alphabet.length; sectionId++) {
const currentChar = alphabet[sectionId];
const users = data.filter((user) =>
user.calle.toUpperCase().indexOf(currentChar) === 0);
if (users.length > 0) {
sectionIds.push(sectionId);
dataBlob[sectionId] = {
character: currentChar
};
rowIds.push([]);
for (let i = 0; i < users.length; i++) {
const rowId = `${sectionId}:${i}`;
rowIds[rowIds.length - 1].push(rowId);
dataBlob[rowId] = users[i];
}
}
}
return {
dataBlob,
sectionIds,
rowIds
};
}
render() {
return (
<View style={{flex: 1, paddingTop: 20}}>
<ListView
style={styles.container}
dataSource={this.state.dataSource}
renderRow={(rowData) => <Row {...rowData} />}
renderSeparator={(sectionId, rowId) => <View key={rowId} />}
style={styles.separator}
renderHeader={() => <Header />}
renderSectionHeader={(sectionData) => <SectionHeader {...sectionData} />}
/>
</View>
);
}
}
AppRegistry.registerComponent('NoTocar', () => NoTocarList);
From your example you have passed the dataSource as null to the ListView. So you need to initialize it first by using
this.state({
dataSource: {}
})
After getting the response from the Api call you need to set the dataSource state by using
this.setState({
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
})
The issue is that you're trying to update the state asynchronously, after a render, but are expecting the result on the first render. Another issue is that you're overwriting the state instead of updating it.
The fetch call in your constructor is async, meaning the constructor is finished and the component (along with its state) is created before that call resolves.
constructor() {
fetch('http://xxxxx.mybluemix.net/get')
// ...
.then(() => {
// ...
// this code gets called after the component is created
// this state is also overwriting already created state
this.state = {
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
}
})
}
Since your data is obtained asynchronously, you can add a check and show a progress indicator while its loading (you should also use setState instead of overwriting the state):
constructor() {
this.state = {
dataSource: null // initialize it explicitly
}
fetch('http://xxxxx.mybluemix.net/get')
// ...
.then(() => {
// ...
// use set state
this.setState({
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
})
})
}
render(){
// on initial render, while the state hasn't been fetched yet
// show the spinner
if (!this.state.dataSource) {
return <ActivityIndicator />
}
return(
<View style={{flex: 1, paddingTop: 20}}>
...
</View>
);
}
I want to animate the depth of the whole Card when the mouse is over it.
I try this (so-so I'm new in React) but I have no idea how to do it:
<Card
linkButton={true}
href="/servicios/"
onClick={Link.handleClick} zDepth={3}
onMouseEnter={this.setState({zDepth={1}})}>
</Card>
Thanks in advance.
5 years later and there is still no correct answer, you do not have to set the component state when it hovers, just use the pseudo-class :hover:
<Card
sx={{
':hover': {
boxShadow: 20, // theme.shadows[20]
},
}}
>
If you want to use styled():
const options = {
shouldForwardProp: (prop) => prop !== 'hoverShadow',
};
const StyledCard = styled(
Card,
options,
)(({ theme, hoverShadow = 1 }) => ({
':hover': {
boxShadow: theme.shadows[hoverShadow],
},
}));
<StyledCard hoverShadow={10}>
<Content />
</StyledCard>
Live Demo
constructor(props) {
super(props);
this.state = { shadow: 1 }
}
onMouseOver = () => this.setState({ shadow: 3 });
onMouseOut = () => this.setState({ shadow: 1 });
<Card
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
zDepth={this.state.shadow}
>
Updated #1
Full example
// StyledCard.js
import React, { Component } from 'react';
import { Card } from 'material-ui/Card';
class StyledCard extends Component {
state: {
shadow: 1
}
onMouseOver = () => this.setState({ shadow: 3 });
onMouseOut = () => this.setState({ shadow: 1 });
render() {
return (
<Card
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
zDepth={this.state.shadow}
>
{this.props.children}
</Card>
);
}
export default StyledCard;
.
// Container.js
import React from 'react';
import StyledCard from './StyledCard';
const Container = () => [
<StyledCard>Card 1</StyledCard>,
<StyledCard>Card 2</StyledCard>,
<StyledCard>Card 3</StyledCard>,
];
export default Container;
UPDATED #2
With HOC
// withShadow.js
import React from 'react';
const withShadow = (Component, { init = 1, hovered = 3 }) => {
return class extends React.Component {
state: {
shadow: init
};
onMouseOver = () => this.setState({ shadow: hovered });
onMouseOut = () => this.setState({ shadow: init });
render() {
return (
<Component
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
zDepth={this.state.shadow}
{...this.props}
/>
);
}
};
};
export default withShadow;
.
// Container.js
import React from 'react';
import { Card } from 'material-ui/Card';
import withShadow from './withShadow';
const CardWithShadow = withShadow(Card, { init: 2, hovered: 4 });
const Container = () => [
<CardWithShadow>Card 1</CardWithShadow>,
<CardWithShadow>Card 2</CardWithShadow>,
<CardWithShadow>Card 3</CardWithShadow>,
];
export default Container;
#Alex Sandiiarov answer didnt work for me. The docs show to use the raised property.
https://material-ui.com/api/card/
class Component extends React.Component{
state = {
raised:false
}
toggleRaised = () => this.setState({raised:!this.state.raised});
render(){
return <Card onMouseOver={this.toggleRaised}
onMouseOut={this.toggleRaised}
raised={this.state.raised}>
...
</Card>
}
}