Which is the react way of complex conditional rendering? - reactjs

Issue
I have a problem with conditional rendering of a component. As far as I can see, there are 2 approaches to doing this. First approach is ugly as it becomes difficult when I have to do multiple && conditions. The second way is clear, but it adds the component itself to the state and further computations with the state value is difficult. E.g checking what is the message value for error.
I have given both the approaches below. Please let me know which would be better. Is there a another approach than both of them?
Application
This is a simple application that renders either 'Main' component or 'Err' component, based on the state of 'err' attribute in first approach and content of the comp attribute in second approach.
Initially Main component is rendered. The err attribute is updated to some value after 2 seconds, which triggers rerendering. At this time, I want Err component to render.
The real application is I have an external api call on componentDidMount and it can either fail or succeed. I have to display different components based on result. It is a little more complicated with multiple state values being updated. I have simplified the issue below for the purpose of demonstration.
Common steps for both types
npx create-react-app react-oop
component/Err.js
import React,{Component} from 'react'
class Err extends Component {
render(){
return(
<div>
Error Component
</div>
)
}
}
export default Err
component/Main.js
import React, {Component} from 'react'
class Main extends Component {
render(){
return(
<div>
Main Component
</div>
)
}
}
export default Main
First approach
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Err from './components/Err'
import Main from './components/Main'
class App extends Component {
constructor(props){
super(props)
this.state = {
err: null
}
this.setError = this.setError.bind(this)
}
setError(){
return(
this.setState(() => {
return({
err: 'Error'
})
})
)
}
componentDidMount(){
setTimeout(this.setError, 2000)
}
render() {
return (
<div className="App">
{
this.state.err ? <Err /> : <Main />
}
</div>
);
}
}
export default App;
Second approach
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Err from './components/Err'
import Main from './components/Main'
class App extends Component {
constructor(props){
super(props)
this.state = {
comp: <Main />
}
this.setError = this.setError.bind(this)
}
setError(){
return(
this.setState(() => {
return({
comp: <Err />
})
})
)
}
componentDidMount(){
setTimeout(this.setError, 2000)
}
render() {
return (
<div className="App">
{this.state.comp}
</div>
);
}
}
export default App;

I definitely recommend the 1st approach. Store data (json), not views (jsx) in your component's state.
Actually there is a 3rd approach that takes the best of both:
use a jsx variable to edit the view (with your logic) before rendering
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Err from './components/Err';
import Main from './components/Main';
class App extends Component {
constructor(props){
super(props);
this.state = {
err: null
};
}
// This way of writing functions saves you the binding
setError = () => this.setState({err: 'Error'})
componentDidMount(){
setTimeout(this.setError, 2000);
}
render() {
let comp = <Main />;
// Put your logic here so your returned JSX is clear
if (this.state.err)
comp = <Err />;
return (
<div className="App">
{comp}
</div>
);
}
}
export default App;

Both approaches are essentially the same but I prefer option 1 as it's simpler to grasp. You can also use something like babel-plugin-jsx-control-statements#choose which makes the React component look simpler:
<Choose>
<When condition={ test1 }>
<Main />
</When>
<When condition={ test2 }>
<AnotherMain />
</When>
<Otherwise>
<Err />
</Otherwise>
</Choose>

Related

Reactjs - how to pass props to Route?

I’m learning React Navigation using React-Router-Dom. I have created a simple app to illustrate the problem:
Inside App.js I have a Route, that points to the url “/” and loads the functional Component DataSource.js.
Inside DataSource.js I have a state with the variable name:”John”. There is also a buttonwith the onclick pointing to a class method that’s supposed to load a stateless component named ShowData.js using Route.
ShowData.js receives props.name.
What I want to do is: when the button in DataSource.js is clicked, the url changes to “/showdata”, the ShowData.js is loaded and displays the props.name received by DataSource.js, and DataSource.js goes away.
App.js
import './App.css';
import {Route} from 'react-router-dom'
import DataSource from './containers/DataSource'
function App() {
return (
<div className="App">
<Route path='/' component={DataSource}/>
</div>
);
}
export default App;
DataSource.js
import React, { Component } from 'react';
import ShowData from '../components/ShowData'
import {Route} from 'react-router-dom'
class DataSource extends Component{
state={
name:' John',
}
showDataHandler = ()=>{
<Route path='/showdata' render={()=><ShowData name={this.state.name}/>}/>
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
}
export default DataSource;
ShowData.js
import React from 'react';
const showData = props =>{
return (
<div>
<p>{props.name}</p>
</div>
)
}
export default showData;
I have tried the following, but, even though the url does change to '/showdata', the DataSource component is the only thing being rendered to the screen:
DataSource.js
showDataHandler = ()=>{
this.props.history.push('/showdata')
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
<Route path='/showdata' render={()=>{<ShowData name={this.state.name}/>}}/>
</div>
)
}
I also tried the following but nothing changes when the button is clicked:
DataSource.js
showDataHandler = ()=>{
<Route path='/showdata' render={()=>{<ShowData name={this.state.name}/>}}/>
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
How can I use a nested Route inside DataSource.js to pass a prop to another component?
Thanks.
EDIT: As user Sadequs Haque so kindly pointed out, it is possible to retrieve the props when you pass that prop through the url, like '/showdata/John', but that's not what I'd like to do: I'd like that the url was just '/showdata/'.
He also points out that it is possible to render either DataSource or ShowData conditionally, but that will not change the url from '/' to '/showdata'.
There were multiple issues to solve and this solution worked as you wanted.
App.js should have all the routes. I used Route params to pass the props to ShowData. So, /showdata/value would pass value as params to ShowData and render ShowData. And then wrapped the Routes with BrowserRouter. And then used exact route to point / to DataSource because otherwise DataSource would still get rendered as /showdata/:name has /
DataSource.js will simply Link the button to the appropriate Route. You would populate DataSourceValue with the appropriate value.
ShowData.js would read and display value from the router prop. I figured out the object structure of the router params from a console.log() of the props object. It ended up being props.match.params
App.js
import { BrowserRouter as Router, Route } from "react-router-dom";
import DataSource from "./DataSource";
import ShowData from "./ShowData";
function App() {
return (
<div className="App">
<Router>
<Route exact path="/" component={DataSource} />
<Route path="/showdata/:name" component={ShowData} />
</Router>
</div>
);
}
export default App;
DataSource.js
import React, { Component } from "react";
import ShowData from "./ShowData";
class DataSource extends Component {
state = {
name: " John",
clicked: false
};
render() {
if (!this.state.clicked)
return (
<button
onClick={() => {
this.setState({ name: "John", clicked: true });
console.log(this.state.clicked);
}}
>
Go!
</button>
);
else {
return <ShowData name={this.state.name} />;
}
}
}
export default DataSource;
ShowData.js
import React from "react";
const ShowData = (props) => {
console.log(props);
return (
<div>
<p>{props.name}</p>
</div>
);
};
export default ShowData;
Here is my scripts on CodeSandbox. https://codesandbox.io/s/zen-hodgkin-yfjs6?fontsize=14&hidenavigation=1&theme=dark
I figured it out. At least, one way of doing it, anyway.
First, I added a route to the ShowData component inside App.js, so that ShowData could get access to the router props. I also included exact to DataSource route, so it wouldn't be displayed when ShowData is rendered.
App.js
import './App.css';
import {Route} from 'react-router-dom'
import DataSource from './containers/DataSource'
import ShowData from './components/ShowData'
function App() {
return (
<div className="App">
<Route exact path='/' component={DataSource}/>
{/* 1. add Route to ShowData */}
<Route path='/showdata' component={ShowData}/>
</div>
);
}
export default App;
Inside DataSource, I modified the showDataHandler method to push the url I wanted, AND added a query param to it.
DataSource.js
import React, { Component } from 'react';
class DataSource extends Component{
state={
name:' John',
}
showDataHandler = ()=>{
this.props.history.push({
pathname:'/showdata',
query:this.state.name
})
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
}
export default DataSource;
And, finally, I modified ShowData to be a Class, so I could use state and have access to ComponentDidMount (I guess is also possible to use hooks here, if you don't want to change it to a Class).
Inside ComponentDidMount, I get the query param and update the state.
ShowData.js
import React, { Component } from 'react';
class ShowData extends Component{
state={
name:null
}
componentDidMount(){
this.setState({name:this.props.location.query})
}
render(){
return (
<div>
<p>{this.state.name}</p>
</div>
)
}
}
export default ShowData;
Now, when I click the button, the url changes to '/showdata' (and only '/showdata') and the prop name is displayed.
Hope this helps someone. Thanks.

On refreshing page browser showing script

Once I am going to refresh pages rather than main route, browser showing script/code instead of ui. Web app made using reactjs v16. I am looking for solutions. Could you please help me?
page after refresh
import React, { Component } from 'react';
class DashboardContainer extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="right_col quick-settings" role="main">
Dashboard
</div>
)
}
}
export default DashboardContainer;
I think you omitted the angle bracket(<>) by mistake when you used Dashborard component. Try this:
import React, { Component } from 'react';
class DashboardContainer extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="right_col quick-settings" role="main">
<Dashboard />
</div>
)
}
}
export default DashboardContainer;

imported component is not displayed

i've a component that i import, but its not displayed on the page.
this is my app.js file. i imported the <player/>component but it is not getting displayed properly on the browser.
import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";
import { player } from "./player";
class App extends Component {
render() {
return (
<div className="App">
<div>
<player />
</div>
</div>
);
}
}
export default App;
this is the contents of the player.js
import React from "react";
import { Button } from "evergreen-ui";
export default class player extends React.Component {
constructor(...args) {
super(...args);
this.state = {
shoot: 0
};
}
shoot() {
this.setState.shoot = Math.floor(Math.random() * Math.floor(3));
}
render() {
return (
<div>
<h1>hello there</h1>
<h1>{this.state.shoot}</h1>
<Button onClick={() => this.shoot}>Shoot another
value</Button>
</div>
);
}
}
In your code, you've exported your player component as a default export
export default class player extemds React.Component
But in your import of it in the other file, you're importing it as a named export
import { player } from "./player";
Try importing it without the curly braces as you would with a default export
import player from "./player";
You are doing two mistakes:
1. Importing the component in the wrong way
2. Rendering the component in the wrong way
Solution
The component should be imported without the curly braces
The react component "player" is supposed to start with capital letters i.e. it should be renamed as Player
Below is the working code I have tried in my local machine. It only modifies App.js
import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";
import Player from "./player"; // imported without curly braces and with capital first letter
class App extends Component {
render() {
return (
<div className="App">
<div>
<Player /> {/* Rendering the correct way */}
</div>
</div>
);
}
}
export default App;
Sidenote
In player.js, you are setting the state in the wrong fashion, it won't work because:
setState is a method and not a object
this is not binded with method shoot. It will throw error something like "cannot read this of undefined" or something
Modify your player.js as following:
import React from "react";
import { Button } from "evergreen-ui";
export default class player extends React.Component {
constructor(...args) {
super(...args);
this.state = {
shoot: 0
};
}
shoot = ()=>{
this.setState({
shoot: Math.floor(Math.random() * Math.floor(3)),
});
}
render() {
return (
<div>
<h1>hello there</h1>
<h1>{this.state.shoot}</h1>
<Button onClick={() => this.shoot()}>Shoot another
value</Button>
</div>
);
}
}
You have two main issues:
1) You export as default and then your import is wrong.
If you export as:
export default class player extemds React.Component
Then you need to import as:
import player from "./player";
2) Components must start uppercase, otherwise React thinks that they are simple HTML tags and not components.
So you must change player to Player everywhere

ReactJS - JSON objects in arrays

I am having a little problem and can't seem to understand how to fix it. So I am trying to create a pokemon app using pokeapi. The first problem is that I can't get my desired objects to display. For example I want to display {pokemon.abilities[0].ability}, but it always shows Cannot read property '0' of undefined but just {pokemon.name} or {pokemon.weight} seems to work. So the problem appears when there is more than 1 object in an array.
import React, {Component} from 'react';
export default class PokemonDetail extends Component{
constructor(props){
super(props);
this.state = {
pokemon: [],
};
}
componentWillMount(){
const { match: { params } } = this.props;
fetch(`https://pokeapi.co/api/v2/pokemon/${params.id}/`)
.then(res=>res.json())
.then(pokemon=>{
this.setState({
pokemon
});
});
}
render(){
console.log(this.state.pokemon.abilities[0]);
const { match: { params } } = this.props;
const {pokemon} = this.state;
return (
<div>
{pokemon.abilities[0].ability}
<img src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${params.id}.png`} />
</div>
);
}
}
And also some time ago I added the router to my app, so I could pass id to other components, but the thing is I want to display pokemonlist and pokemondetail in a single page, and when you click pokemon in list it fetches the info from pokeapi and display it in pokemondetail component. Hope it makes sense.
import React, { Component } from 'react';
import { BrowserRouter as Router, Route} from 'react-router-dom';
import './styles/App.css';
import PokemonList from './PokemonList';
import PokemonDetail from './PokemonDetail';
export default class App extends Component{
render(){
return <div className="App">
<Router>
<div>
<Route exact path="/" component={PokemonList}/>
<Route path="/details/:id" render={(props) => (<PokemonDetail {...props} />)}/>
</div>
</Router>
</div>;
}
}
In case componentWillMount(), An asynchronous call to fetch data will not return before the render happens. This means the component will render with empty data at least once.
To handle this we need to set initial state which you have done in constructor but it's not correct. you need to provide default values for the abilities which is an empty array.
So change it to
this.state = {
pokemon: {
abilities: []
}
}
And then inside render method before rendering you need to verify that it's not empty
render() {
return (
(this.state.pokemon.abilities[0]) ?
<div>
{console.log(this.state.pokemon.abilities[0].ability)}
<img src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png`} />
</div> :
null
);
}
It is common in React that you always need to safe-check for existence of data before rendering, especially when dealing with API data. At the time your component is rendered, the state is still empty. Thus this.state.pokemon.abilities is undefined, which leads to the error. this.state.pokemon.name and this.state.pokemon.weight manage to escape same fate because you expect them to be string and number, and don't dig in further. If you log them along with abilities, they will be both undefined at first.
I believe you think the component will wait for data coming from componentWillMount before being rendered, but sadly that's not the case. The component will not wait for the API response, so what you should do is avoid accessing this.state.pokemon before the data is ready
render(){
const { match: { params } } = this.props;
const {pokemon} = this.state;
return (
<div>
{!!pokemon.abilities && pokemon.abilities[0].ability}
<img src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${params.id}.png`} />
</div>
);
}

Watching state from child component React with Material UI

New to React. Just using create-react-app and Material UI, nothing else.
Coming from an Angular background.
I cannot communicate from a sibling component to open the sidebar.
I'm separating each part into their own files.
I can get the open button in the Header to talk to the parent App, but cannot get the parent App to communicate with the child LeftSidebar.
Header Component
import React, {Component} from 'react';
import AppBar from 'material-ui/AppBar';
import IconButton from 'material-ui/IconButton';
import NavigationMenu from 'material-ui/svg-icons/navigation/menu';
class Header extends Component {
openLeftBar = () => {
// calls parent method
this.props.onOpenLeftBar();
}
render() {
return (
<AppBar iconElementLeft={
<IconButton onClick={this.openLeftBar}>
<NavigationMenu />
</IconButton>
}
/>
);
}
}
export default Header;
App Component -- receives event from Header, but unsure how to pass dynamic 'watcher' down to LeftSidebar Component
import React, { Component } from 'react';
import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import RaisedButton from 'material-ui/RaisedButton';
import Drawer from 'material-ui/Drawer';
import MenuItem from 'material-ui/MenuItem';
// components
import Header from './Header/Header';
import Body from './Body/Body';
import Footer from './Footer/Footer';
import LeftSidebar from './LeftSidebar/LeftSidebar';
class App extends Component {
constructor() {
super() // gives component context of this instead of parent this
this.state = {
leftBarOpen : false
}
}
notifyOpen = () => {
console.log('opened') // works
this.setState({leftBarOpen: true});
/*** need to pass down to child component and $watch somehow... ***/
}
render() {
return (
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
<div className="App">
<Header onOpenLeftBar={this.notifyOpen} />
<Body />
<LeftSidebar listenForOpen={this.state.leftBarOpen} />
<Footer />
</div>
</MuiThemeProvider>
);
}
}
export default App;
LeftSidebar Component - cannot get it to listen to parent App component - Angular would use $scope.$watch or $onChanges
// LeftSidebar
import React, { Component } from 'react';
import Drawer from 'material-ui/Drawer';
import MenuItem from 'material-ui/MenuItem';
import IconButton from 'material-ui/IconButton';
import NavigationClose from 'material-ui/svg-icons/navigation/close';
class LeftNavBar extends Component {
/** unsure if necessary here **/
constructor(props, state) {
super(props, state)
this.state = {
leftBarOpen : this.props.leftBarOpen
}
}
/** closing functionality works **/
close = () => {
this.setState({leftBarOpen: false});
}
render() {
return (
<Drawer open={this.state.leftBarOpen}>
<IconButton onClick={this.close}>
<NavigationClose />
</IconButton>
<MenuItem>Menu Item</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
</Drawer>
);
}
}
export default LeftSidebar;
Free your mind of concepts like "watchers". In React there is only state and props. When a component's state changes via this.setState(..) it will update all of its children in render.
Your code is suffering from a typical anti-pattern of duplicating state. If both the header and the sibling components want to access or update the same piece of state, then they belong in a common ancestor (App, in your case) and no where else.
(some stuff removed / renamed for brevity)
class App extends Component {
// don't need `constructor` can just apply initial state here
state = { leftBarOpen: false }
// probably want 'toggle', but for demo purposes, have two methods
open = () => {
this.setState({ leftBarOpen: true })
}
close = () => {
this.setState({ leftBarOpen: false })
}
render() {
return (
<div className="App">
<Header onOpenLeftBar={this.open} />
<LeftSidebar
closeLeftBar={this.close}
leftBarOpen={this.state.leftBarOpen}
/>
</div>
)
}
}
Now Header and LeftSidebar do not need to be classes at all, and simply react to props, and call prop functions.
const LeftSideBar = props => (
<Drawer open={props.leftBarOpen}>
<IconButton onClick={props.closeLeftBar}>
<NavigationClose />
</IconButton>
</Drawer>
)
Now anytime the state in App changes, no matter who initiated the change, your LeftSideBar will react appropriately since it only knows the most recent props
Once you set the leftBarOpen prop as internal state of LeftNavBar you can't modify it externally anymore as you only read the prop in the constructor which only run once when the component initialize it self.
You can use the componentWillReceiveProps life cycle method and update the state respectively when a new prop is received.
That being said, i don't think a Drawer should be responsible for being closed or opened, but should be responsible on how it looks or what it does when its closed or opened.
A drawer can't close or open it self, same as a light-Ball can't turn it self on or off but a switch / button can and should.
Here is a small example to illustrate my point:
const LightBall = ({ on }) => {
return (
<div>{`The light is ${on ? 'On' : 'Off'}`}</div>
);
}
const MySwitch = ({ onClick, on }) => {
return (
<button onClick={onClick}>{`Turn the light ${!on ? 'On' : 'Off'}`}</button>
)
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
lightOn: false
};
}
toggleLight = () => this.setState({ lightOn: !this.state.lightOn });
render() {
const { lightOn } = this.state;
return (
<div>
<MySwitch onClick={this.toggleLight} on={lightOn} />
<LightBall on={lightOn} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Resources