I have a React Component named Chart. I want to render different charts in it based on the prop passed from their parent, but having the exactly same config. What is the best way to do it?
class Chart extends Component {
static propTypes = {
type: PropTypes.string.isRequired
}
state = {
chartData: {
// some data here
}
}
render(){
return(
this.props.type === 'line' ?
{ <Line
data={this.state.chartData}
options={{
title: {
display: true,
text: 'Cities I\'ve lived in',
fontSize: 25
},
legend: {
display: true,
position: 'right',
labels: {
fontColor: '#000'
}
}
}}
/> } : this.props.type === 'bar' ?
{ <Bar
//same stuff here as <Line />
/>
} : this.props.type === 'pie' ?
{ <Pie
//same stuff here as <Line />
/> }
}
);
}
}
Don't think it matters since the question is generic, but I'm using react-chartjs-2 / chart.js 2 library for rendering charts.
Note: I've tried using a variable name in place of Line/Bar/Pie, but it doesn't work if I'm using JSX or rendering a non-html tag. Better ways to solve it while using JSX and non-html tags are also welcome.
Two things:
Use stateless component, in your example you are not using state.
Use switch instead of if statements.
Your new component, ...chart are the destructured props. Read more about destructuring on MDN.
// Chart.js - new component
export const Chart = ({ type, ...chart }) => {
switch(type) {
case 'line':
return <Line {...chart} />
case 'bar':
return <Bar {...chart} />
case 'pie':
return <Pie {...chart} />
default:
return null;
}
}
Example usage
// App.js
render() {
return (
<div>
<Chart type="line" options={optionsObject} whateverProp="whatever" />
<Chart type="bar" options={optionsObject} whateverProp="whatever" />
<Chart type="pie" options={optionsObject} whateverProp="whatever" />
</div>
)
}
import React, { Component } from 'react';
class Chart extends Component {
components = {
Line: Line,
Bar: Bar,
Pie: Pie
};
render() {
const TagName = this.components[this.props.tag || 'Line'];
return
<TagName
data={this.state.chartData}
options={{
title: {
display: true,
text: 'Cities I\'ve lived in',
fontSize: 25
},
legend: {
display: true,
position: 'right',
labels: {
fontColor: '#000'
}
}
}}
/>
}
}
export default Chart;
// Call MyComponent using prop tag with required prop Line or Bar or Pie
<Chart tag='Line' />
NOTE:- You could create it as a stateless functional component as well.
I prefer to have each chart as a component, so you can do chart's specific configuration in that component and your code will be more close to React codes modularity goal, so for Line chart as example:
class LineChart extends Component {
const chartLegend = {
display: true,
position: 'right',
labels: {
fontColor: '#000'
}};
const chartTitle = {
display: true,
text: 'Cities I\'ve lived in',
fontSize: 25
};
const chartOptions = {
title: chartTitle ,
legend: chartLegend
}
render(){
return(
<Line
data={this.props.chartData}
options={chartOptions }
/>
);
}
}
Now for chat Component as container component you can use switch case or guard conditions as #loelsonk also said, I prefer to have guard conditions besides switch:
class Chart extends Component {
static propTypes = {
type: PropTypes.string.isRequired
}
state = {
chartData: {
// some data here
}
}
render(){
if (this.props.type === 'line')
return <LineChart chartData={...}/>;
if (this.props.type === 'bar')
return <BarChart chartData={...}/>;
if (this.props.type === 'pie')
return <PieChart chartData={...}/>;
}
}
this way you can easily replace any chart implementation any time that needed.
Related
Lets say I already build a component with google chart in ReactJS, and I want to make the Legend to show/hide data by using a js script shown here.
How and where should I put this piece of code to react with my chart?
My component look something like this:
PlayerCountStat
//Skipping all the imports and useEffect code...
return (
<Chart
chartType="LineChart"
width="100%"
height="400px"
data={dateData}
options={options}
chartPackages={["corechart", "controls"]}
controls={[
{
controlType: "ChartRangeFilter",
options: {
filterColumnIndex: 0,
ui: {
chartType: "LineChart",
chartOptions: {
chartArea: { width: "90%", height: "50%" },
hAxis: { baselineColor: "none" },
},
},
},
controlPosition: "bottom",
controlWrapperParams: {
state: {
range: {
start: {startFilterDate},
end: {endFilterDate}
},
},
},
},
]}
/>
);
}
export default PlayerCountStat
And the Routing is like this
function App() {
return (
<BrowserRouter>
<Route path="/PlayerCountStat">
<Navbar/>
<Page content={PlayerCountStat}/>
</Route>
</Switch>
</BrowserRouter>
);
}
export default App;
Assuming you're using React Google Charts, there is a prop called chartEvents
<Chart
chartType="LineChart"
width="100%"
height="400px"
data={data}
options={options}
chartEvents={[
{
eventName: 'select' // Change this to applicable event,
callback: ({ chartWrapper }) => {
// Add your logic
}
}
]}
/>
I found this codepen that might be what you're looking to accomplish: https://codesandbox.io/s/d9drj?file=/index.js
I have two components: LeagueSelect and TeamSelect.
All I'm trying to do right now is pass the checkedLeagues state from LeagueSelect to TeamSelect.
It's currently setup to have the checkboxes in TeamSelect be checked if the corresponding league is checked.
The issue: the state passes from LeagueSelect to TeamSelect inconsistently.
This is a video of what it looks like:
https://streamable.com/2i06g
When a box is unchecked, the state updates 'in team', as you can see in the console.log, but, when you try to check the same box again the state does not update in team.
I initially tried to implement this with redux, thought this issue was a redux issue, moved to directly passing state to the child component, and realized that the issue must be somewhere else.
This is my LeagueSelect component:
import React, { Component } from 'react';
import { View, Text, Modal, TouchableHighlight, FlatList, Button } from 'react-native'
import { loadLeagues } from '../actions'
import { connect } from 'react-redux'
import Check from './CheckBox'
import axios from "axios"
import { loadCards, loadTeams, changeLeagues } from '../actions'
import { Icon } from 'native-base'
import TeamSelect from './TeamSelect'
class LeagueSelect extends Component {
constructor(props) {
super(props)
this.state = {
modalVisible: false,
checked: [],
checkedLeagues: [],
checkMessage: ''
}
}
setModalVisible(visible) {
this.setState({modalVisible: visible})
if(this.state.checked.length === 0) {
this.props.league.map(
(v, i) => {
this.state.checked.push(true)
this.state.checkedLeagues.push(v.acronym)
}
)
}
this.setState({ checkMessage: '' })
}
changeCheck = (index, acronym) => {
//local variable to create query param
firstString = []
//adds to local variable if not checked, takes out variable if checked
if(!this.state.checkedLeagues.includes(acronym)) {
firstString.push(acronym)
} else {
firstString.filter(v => { return v !== acronym})
}
//adds leagues that could be in the current state that were not just passed in
this.state.checkedLeagues.map(
(v, i) => {
if(v !== acronym) {
firstString.push(v)
}
}
)
//updates checked leagues state
//makes api call with parameters set
//prevents all leagues being unselected
if(acronym === this.state.checkedLeagues[0] && firstString.length === 0) {
this.setState({ checkMessage: `Don't you want something to look at?` })
} else {
if(!this.state.checkedLeagues.includes(acronym)){
this.state.checkedLeagues[this.state.checkedLeagues.length] = acronym
this.setState({ checkedLeagues: this.state.checkedLeagues })
} else {
newChecked = this.state.checkedLeagues.filter(v => { return v !== acronym})
this.setState({checkedLeagues: newChecked})
}
//updating the check
this.state.checked[index] = !this.state.checked[index]
this.setState({ checked: this.state.checked })
queryString = []
firstString.map(
(v, i) => {
if (queryString.length < 1) {
queryString.push(`?league=${v}`)
} else if (queryString.length >= 1 ) {
queryString.push(`&league=${v}`)
}
}
)
axios.get(`http://localhost:4000/reports${queryString.join('')}`)
.then(response => {
this.props.loadCards(response.data)
})
}
}
render() {
return (
<View style={{ position: 'relative'}}>
<Text
style={{
paddingTop: 8,
paddingLeft: 5,
fontSize: 15
}}
>Leagues</Text>
<View
style={{
flexDirection:"row",
}}
>
{this.props.league === null ?'' : this.props.league.map(
(v, i) => {
return(
<View
key={i}
style={{
alignSelf: 'flex-end',
flexDirection:"row",
top: 4,
}}
>
<Check
checked={this.state.checked[i]}
index={i}
value={v.acronym}
changeCheck={this.changeCheck}
/>
<Text
style={{
paddingLeft: 23,
}}
>{v.acronym}</Text>
</View>
)
}
)}
</View>
<Text
style={{
paddingLeft: 10,
paddingTop: 12,
fontStyle: 'italic',
color: '#F4AF0D'
}}
>{this.state.checkMessage}</Text>
<TeamSelect checkedLeagues={this.state.checkedLeagues}/>
</View>
</View>
);
}
}
export default LeagueSelect
This is my TeamSelect component:
import React, { Component } from 'react'
import { Text, View } from 'react-native'
import { connect } from 'react-redux'
import { loadTeams, loadLeagues } from '../actions'
import Check from './CheckBox'
class TeamSelect extends Component {
constructor(props) {
super(props)
this.state = {
checked: [],
checkedTeams: [],
setOnce: 0
}
}
render() {
console.log('in team', this.props.checkedLeagues)
return(
<View>
{
this.props.team === null ?'' : this.props.team.map(
(v, i) => {
return(
<View key={i}>
<Check
checked={ this.props.checkedLeagues.includes(v.league.acronym) ? true : false }
index={i}
value={v.team_name}
changeCheck={this.changeCheck}
/>
{ v.team_name === undefined ? null :
<Text>{v.team_name}</Text>}
</View>
)
}
)
}
</View>
)
}
}
export default TeamSelect
this.setState({ checkedLeagues: this.state.checkedLeagues })
Statements like these can cause issues as you are mutating and setting the state to the same object. The reference to checked leagues doesn't get updated and react may not trigger a render. Use this instead
this.setState({ checkedLeagues: [...this.state.checkedLeagues] })
But this whole approach to the problem is wrong, you should use one leagues object that has a checked property to it, and pass it down.
make you league object look like this,
const leagues = [
{
acronym: 'abc',
checked: false,
teams: [ ...array of teams here ]
},
...
]
When you pass it down to TeamSelect, you can map it like this
const { leagues } = this.props
{leagues && leagues.map((league, i) => league.teams.map((team, j) (
<View key={team.team_name}>
<Check
checked={league.checked}
index={i + i * j}
value={team.team_name}
changeCheck={() => this.changeCheck(i, j)}
/>
{team.team_name && <Text>{team.team_name}</Text>}
</View>)))}
Same with leagueSelect, you can map leagues like this:
const { leagues } = this.state
{leagues.map((league, i) => (
<View
key={league.acronym}
style={{
alignSelf: 'flex-end',
flexDirection:"row",
top: 4,
}}>
<Check
checked={league.checked}
index={i}
value={league.acronym}
changeCheck={this.changeCheck}
/>
<Text
style={{
paddingLeft: 23,
}}
>{league.acronym}</Text>
</View>
)
)}
Note: leagues have to be copied from props to state for you to mutate it. I just typed this so it will need some changes before it runs, it's just meant to show you the "react way" of coding this.
https://reactjs.org/docs/lifting-state-up.html
I am creating a component that animates a group of buttons in an elastic staggered way using react-native-pose. The buttons them selves use pose to animate the pressed state. I have almost got it looking how I want it although I'm not sure I'm doing things correctly.
This is what I want to achhieve...
... However the text looks awful like its got jpg artefacts on it :
App.js
import React, { Component } from 'react';
import styled from 'styled-components';
import posed from 'react-native-pose';
import Button from './src/components/Button';
const ScreenContainer = styled.View({
flex: 1,
padding: 20,
marginTop: 100
});
const Buttons = posed.View({
visible: {
staggerChildren: 100
},
hidden: {
staggerChildren: 100
}
});
export default class App extends Component {
state = {
buttonPose: 'hidden'
};
items = [
{ id: 0, label: 'One' },
{ id: 1, label: 'Two' },
{ id: 2, label: 'Three' }
];
componentDidMount = () => {
this.setState({
buttonPose: 'visible'
});
};
render() {
return (
<ScreenContainer>
<Buttons pose={this.state.buttonPose}>
{this.items.map(item => (
<Button label={item.label} key={item.id} />
))}
</Buttons>
</ScreenContainer>
);
}
}
Button.js
import React, { PureComponent } from 'react';
import { TouchableWithoutFeedback } from 'react-native';
import styled from 'styled-components';
import posed from 'react-native-pose';
const Container = styled(
posed.View({
visible: {
opacity: 1,
x: 0
},
hidden: {
opacity: 0,
x: -100
}
})
)({
marginBottom: 20
});
const Background = styled(
posed.View({
// If I comment out these poses the problem goes away
pressIn: {
scale: 1.1
},
pressOut: {
scale: 1
}
})
)({
padding: 20,
backgroundColor: '#f9415d',
borderRadius: 10
});
const Label = styled.Text({
fontSize: 18,
color: 'white',
textAlign: 'center'
});
export default class Button extends PureComponent {
state = {
buttonPose: 'pressOut'
};
onPressIn = () => {
this.setState({
buttonPose: 'pressIn'
});
};
onPressOut = () => {
this.setState({
buttonPose: 'pressOut'
});
};
componentDidMount = () => {};
render() {
const { onPressIn, onPressOut } = this;
const { buttonPose } = this.state;
const { label } = this.props;
return (
<Container>
<TouchableWithoutFeedback onPressIn={onPressIn} onPressOut={onPressOut}>
<Background pose={buttonPose} withParent={false}>
<Label>{label}</Label>
</Background>
</TouchableWithoutFeedback>
</Container>
);
}
}
Can anyone offer any insight into why the text and also the rounded corners look so artifacted and low res?
I am testing out react-native and I am trying to make a simple lineChart which redraws on props change. I have a parent component HomeScreen which passes an array of integers as props to the LineChart child component. However, the LineChart is never drawn.
I have tried passing in an already initialized array with dummy values. The lineChart child component will then render, but it won't re-render on subsequent state changes.
I have checked the actual values of state and props in react-devtools, and the childcomponent does receive the props and the state is updated. How can I make the chart render the props I pass it?
UPDATE: So I took the advice from the replies here, and made the component functional. The chart now renders, but there is still something wrong with the prop type. I'll investigate further and read up on the react-native-svg documentation. Thanks!
enter code here
import React from 'react';
import { View } from 'react-native';
import { LineChart, Grid } from 'react-native-svg-charts'
const BeatChart = ({ data }) => (
<LineChart
style={{ height: 200 }}
data={data}
svg={{ stroke: 'rgb(0, 255, 255)' }}
contentInset={{ top: 20, bottom: 20 }}
>
<Grid />
</LineChart>
);
export default BeatChart;
You don't need state in this example, so you could just use the functional component.
import React from 'react';
import { View } from 'react-native';
import { LineChart, Grid } from 'react-native-svg-charts'
const BeatChart = ({ data }) => (
<LineChart
style={{ height: 200 }}
data={data}
svg={{ stroke: 'rgb(0, 255, 255)' }}
contentInset={{ top: 20, bottom: 20 }}
>
<Grid />
</LineChart>
);
export default BeatChart;
since the render occurs on every change in props, you can comment this part of the code.
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.data !== prevState.data) {
return { data: nextProps.data };
}
return null;
}
even if you want to implement it, use componentWillmount if needed.
otherwise the code works fine.
so your final code be like:
import React from 'react';
import { View } from 'react-native';
import { LineChart, Grid } from 'react-native-svg-charts'
class BeatChart extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
data: [],//your data
}
};
// static getDerivedStateFromProps(nextProps, prevState) {
// if (nextProps.data != prevState.data) {
// return { this.setState({data: nextProps.data}) };
// }
// return null;
// }
render() {
const arr = this.state.data;
return (
<LineChart
style={{ height: 200 }}
data={arr}
svg={{ stroke: 'rgb(0, 255, 255)' }}
contentInset={{ top: 20, bottom: 20 }}
>
<Grid />
</LineChart>
)
}
}
export default BeatChart;
I am working on a react native app that uses Composite Experimental Navigation (CardStack + Tabs) and redux for state management. I was able to create Tab based Navigation but the issue that i am facing now is when i switch between tabs the component is unmounted and it re-render every time.
Problems
Lets say i have scrolled down several posts and when i change Tab it will start from top. (Workaround could be to store scroll position in redux state).
Here is the sample code i am using for Navigation Tabbed Experimental Navigation
You must change approach for TabBar.
Basically you want Navigator per Tab, so you can have route stack for each Tab and one Navigator to contain Tabs (TabBar). You also want to set initial route stack for TabBar and jump between those routes.
It is important to understand difference between Navigator methods.
When you pop route, it's unmounted, and active index is moved to
last one. As last one is kept in state it will be restored as it was
previously rendered (most likely, it can happen props changed). Going again to popped route will rerender scene entirely (this happens to you).
When you push route, nothing is unmounted, new route is mounted.
When you jumpToIndex route, again nothing in unmounted, thus jumping
between routes restores scenes as they were (again, if props changed
scene will be rerendered).
So, I don't think this is correct:
I was able to create Tab based Navigation but the issue that i am facing now is when i switch between tabs the component is unmounted and it re-render every time.
... you unmount routes with wrong navigation actions.
Also what is different now, NavigationCardStack doesn't actual create it's own state, it is passed from outside, which gives you great flexibility. And also good thing is that you can use reducers provided by Facebook for common actions (such as push, pop, jumpToIndex; they are part of Navigation Utils).
You have full example on how to create navigationState and it reducers here, so I am not going to explain that, just going to give idea how to solve your problem.
[UPDATE] Example works now!
import React from 'react';
import { NavigationExperimental, View, Text, StyleSheet } from 'react-native';
const {
CardStack: NavigationCardStack,
StateUtils: NavigationStateUtils,
} = NavigationExperimental;
const style = StyleSheet.create({
screen: {
flex: 1,
},
screenTitle: {
marginTop: 50,
fontSize: 18,
},
pushNewScreenLabel: {
marginVertical: 10,
fontSize: 15,
fontWeight: "bold",
},
goBackLabel: {
fontSize: 15,
},
tabBarWrapper: {
position: 'absolute',
height: 50,
bottom: 0,
left: 0,
right: 0,
top: null,
backgroundColor: 'grey',
flexDirection: 'row',
flex: 0,
alignItems: 'stretch',
},
tabBarItem: {
flex: 1,
justifyContent: 'center',
backgroundColor: 'red',
},
});
export class TabBar extends React.Component {
constructor(props, context) {
super(props, context);
this.jumpToTab = this.jumpToTab.bind(this);
// Create state
this.state = {
navigationState: {
// Active route, will be rendered as default
index: 0,
// "tab-s" represents route objects
routes: [
{ name: 'Tab1', key: '1' },
{ name: 'Tab2', key: '2' },
{ name: 'Tab3', key: '3' },
{ name: 'Tab4', key: '4' }],
},
};
}
jumpToTab(tabIndex) {
const navigationState = NavigationStateUtils.jumpToIndex(this.state.navigationState, tabIndex);
this.setState({ navigationState });
}
renderScene({ scene }) {
return <Tab tab={scene.route} />;
}
render() {
const { navigationState } = this.state;
return (
<View style={style.screen}>
<NavigationCardStack
onNavigate={() => {}}
navigationState={navigationState}
renderScene={this.renderScene}
/>
<View style={style.tabBarWrapper}>
{navigationState.routes.map((route, index) => (
<TabBarItem
key={index}
onPress={this.jumpToTab}
title={route.name}
index={index}
/>
))}
</View>
</View>
);
}
}
class TabBarItem extends React.Component {
static propTypes = {
title: React.PropTypes.string,
onPress: React.PropTypes.func,
index: React.PropTypes.number,
}
constructor(props, context) {
super(props, context);
this.onPress = this.onPress.bind(this);
}
onPress() {
this.props.onPress(this.props.index);
}
render() {
return (
<Text style={style.tabBarItem} onPress={this.onPress}>
{this.props.title}
</Text>);
}
}
class Tab extends React.Component {
static propTypes = {
tab: React.PropTypes.object,
}
constructor(props, context) {
super(props, context);
this.goBack = this.goBack.bind(this);
this.pushRoute = this.pushRoute.bind(this);
this.renderScene = this.renderScene.bind(this);
this.state = {
navigationState: {
index: 0,
routes: [{ key: '0' }],
},
};
}
// As in TabBar use NavigationUtils for this 2 methods
goBack() {
const navigationState = NavigationStateUtils.pop(this.state.navigationState);
this.setState({ navigationState });
}
pushRoute(route) {
const navigationState = NavigationStateUtils.push(this.state.navigationState, route);
this.setState({ navigationState });
}
renderScene({ scene }) {
return (
<Screen
goBack={this.goBack}
goTo={this.pushRoute}
tab={this.props.tab}
screenKey={scene.route.key}
/>
);
}
render() {
return (
<NavigationCardStack
onNavigate={() => {}}
navigationState={this.state.navigationState}
renderScene={this.renderScene}
/>
);
}
}
class Screen extends React.Component {
static propTypes = {
goTo: React.PropTypes.func,
goBack: React.PropTypes.func,
screenKey: React.PropTypes.string,
tab: React.PropTypes.object,
}
constructor(props, context) {
super(props, context);
this.nextScreen = this.nextScreen.bind(this);
}
nextScreen() {
const { goTo, screenKey } = this.props;
goTo({ key: `${parseInt(screenKey) + 1}` });
}
render() {
const { tab, goBack, screenKey } = this.props;
return (
<View style={style.screen}>
<Text style={style.screenTitle}>
{`Tab ${tab.key} - Screen ${screenKey}`}
</Text>
<Text style={style.pushNewScreenLabel} onPress={this.nextScreen}>
Push Screen into this Tab
</Text>
<Text style={style.goBackLabel} onPress={goBack}>
Go back
</Text>
</View>
);
}
}
## TBD little more clean up..