Lazy load a React component from an array of objects - javascript

I have made for me a Tutorial-Project where I collect various React-Examples from easy to difficult. There is a "switch/case" conditional rendering in App.js, where I - depending on the ListBox ItemIndex - load and execute the selected Component.
I am trying to optimize my React code by removing the "switch/case" function and replacing it with a two dimensional array, where the 1st column contains the Component-Name 2nd column the Object. Further I would like to lazy-load the selected components.
Everything seems to work fine, I can also catch the mouse events and also the re-rendering begins but the screen becomes white... no component rendering.
App.js
import SampleList, { sampleArray } from './SampleList';
class App extends React.Component {
constructor(props) {
super(props);
this.selectedIndex = -1;
}
renderSample(index) {
if((index >= 0) && (index < sampleArray.length)) {
return React.createElement(sampleArray[index][1])
} else {
return <h3>Select a Sample</h3>;
}
}
render() {
return (
<header>
<h1>React Tutorial</h1>
<SampleList myClickEvent={ this.ClickEvent.bind(this) }/>
<p />
<div>
<Suspense> /**** HERE WAS MY ISSUE ****/
{ this.renderSample(this.selectedIndex) }
</Suspense>
</div>
</header>
);
}
ClickEvent(index) {
this.selectedIndex = index;
this.forceUpdate();
}
}
SampleList.js
import React from 'react';
const SimpleComponent = React.lazy(() => import('./lessons/SimpleComponent'));
const IntervalTimerFunction = React.lazy(() => import('./lessons/IntervalTimerFunction'));
const sampleArray = [
["Simple Component", SimpleComponent],
["Interval Timer Function", IntervalTimerFunction]
];
class SampleList extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.selectOptions = sampleArray.map((Sample, Index) =>
<option>{ Sample[0] }</option>
);
}
render() {
return (
<select ref={this.myRef} Size="8" onClick={this.selectEvent.bind(this)}>
{ this.selectOptions }
</select>
);
}
selectEvent() {
this.props.myClickEvent(this.myRef.current.selectedIndex);
}
}
export default SampleList;
export { sampleArray };

You have several issues in that code:
If you use React.lazy to import components dynamically, use Suspense to show a fallback;
The select can listen to the change event, and receive the value of the selected option, that is convenient to pass the index in your case;
Changing a ref with a new index doesn't trigger a re-render of your components tree, you need to perform a setState with the selected index;
I suggest you to switch to hooks, to have some code optimizations;
Code:
import React, { Suspense, useState, useMemo } from 'react';
const SimpleComponent = React.lazy(() => import('./lessons/SimpleComponent'));
const IntervalTimerFunction = React.lazy(() =>
import('./lessons/IntervalTimerFunction'));
const sampleArray = [
['Simple Component', SimpleComponent],
['Interval Timer Function', IntervalTimerFunction],
];
export default function App() {
const [idx, setIdx] = useState(0);
const SelectedSample = useMemo(() => sampleArray[idx][1], [idx]);
const handleSelect = (idx) => setIdx(idx);
return (
<Suspense fallback={() => <>Loading...</>}>
<SampleList handleSelect={handleSelect} />
<SelectedSample />
</Suspense>
);
}
class SampleList extends React.Component {
constructor(props) {
super(props);
}
selectEvent(e) {
this.props.handleSelect(e.target.value);
}
render() {
return (
<select ref={this.myRef} Size="8" onChange={this.selectEvent.bind(this)}>
{sampleArray.map((sample, idx) => (
<option value={idx}>{sample[0]}</option>
))}
</select>
);
}
}
Working example HERE

Related

Attempting to place data from an API onto a modal in React

I'm attempting to put data that I'm getting from an API onto a modal that will appear whenever a button is clicked.
How is this done? I'm able to use the data from the API without the modal, so I know it's not an issue with the syntax of my componentDidMount(). Not sure what the issue is and how it can be resolved.
import React from 'react';
import './App.css';
import Nav from './Nav';
import Meal from './Meal';
import meals from './Meals';
import Modal1 from './Modal'
function App() {
const mealArr = meals.map(item => <Meal food={item.food} picture={item.picture} type={item.id} />)
return (
<div className="content">
<Nav />
{mealArr}
<Modal1 isOpen={false}/>
</div>
);
}
export default App;
import React from 'react';
import Modal from 'react-modal';
class Modal1 extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
isLoaded: false
}
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(json => {
this.setState({
isLoaded: true,
items: json
})
})
}
render() {
const allItems = this.state.items;
let itemArr = allItems.map(item =>
<div>
<ul>
<li key={item.id}>{item.name}</li>
</ul>
</div>)
return (
<div>
<Modal>
{itemArr}
</Modal>
</div>
)
}
}
export default Modal1;
import React, {Component} from 'react';
import Modal1 from 'react-modal';
class Meal extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false
}
this.handleClick = this.handleClick.bind(this);
this.turnOff = this.turnOff.bind(this);
}
handleClick() {
this.setState({isOpen: true})
}
turnOff() {
this.setState({isOpen: false})
}
render() {
return (
<div className="meal-container">
<h2>{this.props.type}</h2>
<h1>{this.props.food}</h1>
<img alt="" src={this.props.picture} />
<p className="steps-button" onClick={this.handleClick}>Steps</p>
<Modal1 className="modal-1" isOpen={this.state.isOpen}/>
</div>
)
}
}
export default Meal;
take a look at allItems, it's an empty array before you get the data from the API.
So, for the first render (before component did mount):
const allItems = this.state.items // ----> const allItems = []
mapping through an empty array will not produce any error and return another empty array, but when you map through an empty array, don't expect to have any item or item.name. so the itemArr is not as your expectation and cause the issue with rendering it.
to avoid from this issue, check your allItems to ensure that the data has arrived.
const allItems = this.state.items;
let itemArr = []
if (allItems.length > 0) {
itemArr = allItems.map(item => (
<div>
<ul>
<li key={item.id}>{item.name}</li>
</ul>
</div>
)
}
return (
<div>
<Modal>
{itemArr}
</Modal>
</div>
)

React component that's rendered dynamically does not rerender on parent state changes

I have a component that I want to run through a non react animation library before render. This has prevented me from going the standard route of just using the standard hide/show logic. I initially tried to use ReactDOM.createPortal but that didn't render the component at all. Using ReactDOM.render, I've gotten the element to render correctly upon completion of the animation and I'm able to successfully propagate changes up to the "parent" state but the state change doesn't propagate back down to the "child". Here's my code:
Html
<div id="root"></div>
<div id="childPlaceholder"></div>
Javascript
import './App.css';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
function App() {
const [data, updateData] = useState(0)
function add(val) {
console.log("add");
updateData(val);
}
function renderSubpage() {
let $el = document.getElementById("childPlaceholder");
// NonReactAnimationLibrary.ShowContainer($el);
ReactDOM.render(<Child number={data} add={add} />, $el);
// ReactDOM.createPortal(<Child number={data} add={add} />, $el);
}
return ( <>
<button onClick={renderSubpage}>
add child
</button>
<div> data: {data}</div>
</>
);
}
function Child(props) {
return <>
<button onClick={()=>{props.add(props.number + 1)}}>add number</button>
<div>child {props.number}</div>
</>
}
export default App;
Is it possible to do this in react?
Update 1:
So I've updated the code per Olivers response, it renders correctly using the portal but the child components still don't rerender on state changes in the Parent Component
const root = document.getElementById("root");
const childRoot = document.getElementById("childPlaceholder");
function Child(args) {
return ReactDOM.createPortal(<>
<div>child: {args.number}</div>
<button onClick={()=>{args.add(args.number+1)}}>Increment base number</button>
</>, childRoot);
}
export default class App extends React.Component {
constructor() {
super();
this.state = { data: 0, number:0 };
}
add = (val)=> {
this.setState({
...this.state,
number: val
});
}
addChild = () => {
this.setState(prevState => ({data: prevState.data + 1}));
}
render() {
const children = Array(this.state.data)
.fill()
.map((_, i) => <Child key={i} number={0} add={this.add}/>);
return (
<div>
<button onClick={this.addChild}>
add child
</button>
<div> data: {this.state.data}</div>
{children}
</div>
);
}
}
ReactDOM.render(<App/>, root);
Update 2:
The culprit was found. Changed
number={0}
to
number={this.state.number}
and it works
React.createPortal must be used inside the render method (I used a class component because I cannot use hooks in the SO example, you can of course use a functional component).
You can use it in the App component like below or in the Child component :
const root = document.getElementById("root");
const childRoot = document.getElementById("childPlaceholder");
function Child({number}) {
return <div>child {number}</div>;
}
class App extends React.Component {
constructor() {
super();
this.state = { data: 0 };
}
addChild = () => {
this.setState(prevState => ({data: prevState.data + 1}));
}
render() {
const children = Array(this.state.data)
.fill()
.map((_, i) => <Child key={i} number={i} />);
return (
<div>
<button onClick={this.addChild}>add child</button>
<div> data: {this.state.data}</div>
{ReactDOM.createPortal(children, childRoot)}
</div>
);
}
}
ReactDOM.render(<App/>, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<div id="childPlaceholder"></div>

how to passing a .JSON in setState in reactJS

I have a problem, when I try to pass a .json like this:
this is my class
import MyForm from './MyForm';
class CreateProject extends React.Component{
constructor(){
super();
this.state = { categories:[]}
}
getCategories(){
API.get(`/categories/public`)
.then(resp=>{
this.setState({categories: resp.data})
})
.catch(err => {
console.log(err)
})
}
ComponentDidMOunt(){
// here show me the API correct like this
// 0:{id:1, name:"categorie one"}
// 1:{id:11, name:"categorie four"}
// 2:{id:19, name:"categorie five"}
// 3:{id:16, name:"categorie six"}
this.getCategories()
}
render(){
return(<div> <MyForm categories={this.state.categories}/></div>)
}
}
my functional component
export const MyForm = ({categories}) =>{
return(
<div>
<select >
{ // here not working because .map not belong a function categories
categories.map(category =>(
<option value={category.id}>{category.name}</option>
))
}
</select>
</div>)
}
how to read a categories inside my functional component using a loop . please something suggestion or a tip
thanks for your attention.
A couple things I noticed
componentDidMount() spelling error and an incorrect import. Should be:
import { MyForm } from './MyForm'
Here's a very similar working example. I'm just using a different api and I have an async function, also added some null checks on categories (might be redundant?).
https://codesandbox.io/s/modern-frog-0wmyu
import React from "react";
import { MyForm } from "./my-form";
class CreateProject extends React.Component {
constructor() {
super();
this.state = { categories: [] };
}
async getCategories() {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const data = await response.json();
this.setState({ categories: data });
}
componentDidMount() {
// here show me the API correct like this
// 0:{id:1, name:"categorie one"}
// 1:{id:11, name:"categorie four"}
// 2:{id:19, name:"categorie five"}
// 3:{id:16, name:"categorie six"}
this.getCategories();
}
render() {
const { categories } = this.state;
return (
<div>
{categories && categories.length > 0 && (
<MyForm categories={categories} />
)}
</div>
);
}
}
export default CreateProject;
MyForm component
import React from "react";
// I'm using the title property, but for your API it should be category.name
export const MyForm = ({ categories }) => (
<select>
{categories &&
categories.map(category => (
<option value={category.id}>{category.title}</option>
))}
</select>
);

React Native: Component rerender but props has not changed

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.

Clear input in stateless React component

I want to implement an X icon inside Input component that will clear the input field. I can easily do it if I control the state. But is it actually possible with stateless component?
I use react-semantic-ui, their stateful components have auto controlled state.
So I want to create an input that can be used like this:
//Controlled
class App extends React.Component {
state = {
value:''
}
onChange = (event, props) => {
this.setState({value: props.value});
}
onClearInput = () => {
this.setState({value: ''});
}
render() {
return (
<MyInput
clearable
value={this.state.value}
onChange={this.onChange}
onClearInput={this.onClearInput}
/>
)
}
}
Or
// Uncontrolled
class App extends React.Component {
onChange = (event, props) => {
doSomething(props.value);
}
render() {
return (
<MyInput
clearable
onChange={this.onChange}
/>
)
}
}
In the second example, clearable feature will not work because we're not controlling the value.
MyInput can be implemented like this:
import React from 'react';
import { Input } from 'semantic-ui-react';
import ClearIcon from './ClearIcon';
function MyInput(props) {
const prepareProps = {...props};
if (props.clearable) {
prepareProps.icon=<ClearIcon onClick={props.onClearInput} />;
delete prepareProps.clearable;
}
delete prepareProps.onClearInput;
return (
<div className="my-input">
<Input {...prepareProps} />
</div>
);
}
...etc.
My problems:
clearable feature must work in both controlled and uncontrolled manner.
clearable feature should not require a handler. It would be nice to just provide a prop and handle the render and behavior of the X button under the hood.
I don't see any way to make this work. Any ideas?
Allowing the user of your component to set the value via props and still being able to clear the input can be easily achieved, e.g. like this:
class MyInput extends React.Component {
constructor(props) {
super(props);
this.state = {value: props.value || ''};
}
handleChange = event => {
const { onChange } = this.props;
this.setState({ value: event.currentTarget.value });
onChange && onChange(event);
};
handleClear = () => {
const { onClearInput } = this.props;
this.setState({ value: "" });
onClearInput && onClearInput();
};
render() {
const { value } = this.state;
const { clearable, onChange, ...inputProps } = this.props;
const clearIcon = clearable && <ClearIcon onClick={this.handleClear} />;
return (
<div className="my-input">
<Input value={value} icon={clearIcon} onChange={this.handleChange} {...inputProps} />
</div>
);
}
}
You could even make it more composable by using an hoc or render props as proposed by #pkuzhel.
Look at this codesandbox example to see it in action.
#Andrey
Would you try this below code? and let me know if that resolves your issue.
import React, { Component } from 'react';
import { Input, Button } from 'semantic-ui-react'
class App extends Component {
clear = () => {
console.log(this.inputRef.target.value);
this.inputRef.target.value = '';
}
render() {
return (
<div className="App">
<Input placeholder='Search...' onChange={(input) => {input.persist(); this.inputRef = input}} />
<Button onClick={this.clear}>Clear</Button>
</div>
);
}
}

Categories

Resources