I am trying to build a lazy loading tabs component that fetches the code/bundle for the tab only when it is clicked. I am trying to use lazy+suspense for it. I would like to animate/color the tab the user clicked on when it is loading to indicate that a tab is being loaded. How can I do that best?
Here is some example code I have whipped up. The bug here is that the tab-header can sometimes get rendered twice when the code is being loaded. How can I avoid the issue and display a loading state on the new tab.
import React, {lazy, Suspense, useState, useReducer} from "react";
import "./App.css";
import classNames from "classnames";
import Spinner from "./Spinner";
const Tokyo = lazy(() => {
return import("./Tokyo");
});
const Mexico = lazy(() => {
return import("./Mexico");
});
const London = lazy(() => {
return import("./London");
});
const App = () => {
const [_, setTab] = useState("Tokyo");
return (
<div className="App">
<header className="App-header">
<Tabs initialTab="Tokyo" onTabChange={setTab}>
<Tab id="Tokyo" name="Tokyo">
<Tokyo />
</Tab>
<Tab id="Mexico" name="Mexico">
<Mexico />
</Tab>
<Tab id="London" name="London">
<London />
</Tab>
</Tabs>
</header>
</div>
);
};
const Tab = () => {
return null;
};
function genClickLog(log, current) {
const set = new Set([current]);
const newLog = [current];
log.forEach(l => {
if (!set.has(l)) {
log.push(l);
newLog.push(l);
}
});
return newLog;
}
function createSuspenseTree(targetTab, log, child, tabs, handleTabChange) {
const head = log.shift();
if (head !== targetTab) {
console.warn(`expect ${head} to be ${targetTab}`);
}
let current = child;
log.forEach(l => {
current = (
<Suspense
fallback={
<Fallback
tabs={tabs}
prevTab={l}
activeTab={targetTab}
onTabChange={handleTabChange}
/>
}
>
{current}
</Suspense>
);
});
return <Suspense fallback={<Spinner />}>{current}</Suspense>;
}
function reducer(state, action) {
switch (action.type) {
case "change":
if (state.current === action.id) {
return state;
}
return {
current: action.id,
prev: state.current,
clickLog: genClickLog(state.clickLog, action.id),
};
case "initial":
return {
current: action.id,
prev: null,
clickLog: [action.id],
};
default:
throw new Error("bad reducer action");
}
}
const Tabs = props => {
const {children, onTabChange, initialTab} = props;
const [state, dispatch] = useReducer(
reducer,
{
clickLog: [],
prev: null,
current: null,
},
{type: "initial", id: initialTab}
);
const handleTabChange = tab => {
dispatch({type: "change", id: tab});
onTabChange(tab);
};
const tabs = React.Children.map(children, x => ({
id: x.props.id,
name: x.props.name,
render: x.props.children,
}));
const child = (
<>
<TabHeader
tabs={tabs}
activeTab={state.current}
onTabChange={handleTabChange}
/>
{tabs.map(x => (
<div key={x.id}>
<TabFrag
id={x.id}
key={x.id}
activeTab={state.current}
render={x.render}
/>
</div>
))}
</>
);
return (
<div className="TabContainer">
{createSuspenseTree(
state.current,
[...state.clickLog],
child,
tabs,
handleTabChange
)}
</div>
);
};
const Fallback = props => {
const {prevTab, activeTab, onTabChange, tabs} = props;
if (prevTab && prevTab !== activeTab) {
return (
<>
<TabHeader
tabs={tabs}
activeTab={prevTab}
loadingTab={activeTab}
onTabChange={onTabChange}
/>
{tabs.map(x => (
<div key={x.id}>
<TabFrag
id={x.id}
key={x.id}
activeTab={prevTab}
render={x.render}
/>
</div>
))}
</>
);
}
return <Spinner />;
};
const TabFrag = props => {
if (props.id === props.activeTab) {
return props.render;
}
return null;
};
const TabHeader = props => {
const {tabs, activeTab, loadingTab, onTabChange} = props;
return (
<div className="TabHeader">
{tabs.map(x => (
<TabItem
id={x.id}
key={x.id}
name={x.name}
active={x.id === activeTab}
loading={x.id === loadingTab}
onTabChange={onTabChange}
/>
))}
</div>
);
};
const TabItem = props => {
const {id, name, loading, active, onTabChange} = props;
const handleTabChange = () => {
onTabChange(id);
};
return (
<div
className={classNames("TabItem", {
ActiveTab: active,
LoadingTab: loading,
})}
onClick={handleTabChange}
>
{name}
</div>
);
};
Related
NewsDetails
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{
state
.filter((a) => a.id === id)
.map((card, index) => (
<>
<div className="card" key={index}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
</>
))
}
</div>
)
}
export default NewsDetail
NewsItem
import React from 'react'
import clock from "../components/assets/img/Clock.svg"
import user from "../components/assets/img/User.svg"
import { Link } from 'react-router-dom'
const NewsItem = (props) => {
const { imageUrl, title, author, content, date, id } = props
return (
<Link className="col-lg-4 p-2" to={`/detail/${id}`}>
<div className="newsItem">
<img src={imageUrl} alt='newsPhoto' />
<h2>{id}</h2>
<div className="itemBody">
<p className='title'>{title}</p>
<div className="line"></div>
<p className='content'>{content}</p>
<div className="itemfooter">
<span><img src={clock} alt='clock' />{date}</span>
<span><img src={user} alt='user' />{author}</span>
</div>
</div>
</div>
</Link>
)
}
export default NewsItem
Home
import React, { useEffect, useState } from "react";
import NewsItem from "./NewsItem";
import SpinnerLoad from "./SpinnerLoad";
import { v4 as uuidv4 } from 'uuid';
const Home = (props) => {
const Category = [
"all",
"business",
"sports",
"world",
"technology",
"entertainment",
"science"
];
const { state, setState} = props;
const [loading, setLoading] = useState(false)
const fetchValue = (category) => {
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
console.log(state);
setLoading(false);
};
// const fetchValue = async () => {
// try {
// const data = await axios
// .get(`https://inshorts-api.herokuapp.com/news?category=sports`)
// .then(res => {
// console.log(res);
// setState(res.data)
// })
// setLoading(true)
// console.log(loading);
// } catch (e) {
// console.log(e);
// }
// }
const CategoryButton = ({ category }) => (
<button onClick={() => fetchValue(category)} style={{ textTransform: 'capitalize' }}>{category}</button>
);
useEffect(() => {
fetchValue('all')
},[])
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value, index) => {
return <CategoryButton category={value} key={index} />;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad/>
:
state.map((data,index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()}
key={index}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default Home;
I have created a project with api. With categories it is possible to change the incoming data, but there is one thing where I want to get more detailed information when I click on the newsItem card. That api doesn't have id value, so I used uuid. Information corresponding to the id value should come with useParams. But it doesn't work. How can I fix this problem?
The first issue is that you are generating a GUID when rendering the state array which won't necessarily correlate to any data you are trying to match/filter by in the NewsDetail component.
state.map((data,index) => (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()} // <-- new id each render cycle
key={index}
/>
))
You want to inject the id property when the data is fetch so that it's a stable reference that lives as long as the data does. In other words, it should be an intrinsic property of the data.
Example:
const fetchValue = async (category) => {
setLoading(true);
try {
const res = await fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`);
const { data } = await res.json();
setState(data.map(el => ({
...el,
id: uuidv4(), // <-- map and inject id here
})));
} catch(error) {
console.log(error);
} finally {
setLoading(false);
}
};
...
state.map((data) => (
<NewsItem
key={data.id} // <-- use as React key
data={data} // <-- pass entire data object as prop
/>
))
NewsItem
const NewsItem = ({ data }) => {
const { imageUrl, title, author, content, date, id } = data;
return (
...
);
};
NewsDetail
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{state
.filter((card) => card.id === id)
.map((card) => (
<div className="card" key={card.id}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
))
}
</div>
);
};
I'm new to React and currently working on a to-do list app. Currently, I'm able to add, delete and edit the to-do list.
I have a problem filtering my to-do list based on categories. The categories I have are all, active and completed.
I'm stuck trying to filter the selected list based on the button clicked.
App.js
import React from "react";
import "./styles.css";
import "./App.css";
import Header from "./components/Header";
import AddTask from "./components/AddTask";
import Task from "./components/Task";
import Filterbtns from "./components/Filterbtns";
import data from "./data";
import { nanoid } from "nanoid";
const FILTER_MAP = {
All: () => true,
Active: (todo) => !todo.completed,
Completed: (todo) => todo.completed
};
const FILTER_NAMES = Object.keys(FILTER_MAP); //keys
function App() {
const [taskList, setTaskList] = React.useState(data);
const [filtered, setFiltered] = React.useState(data); //state to be filtered
const filteredListName = FILTER_NAMES;
const [activeList, setActiveList] = React.useState(filteredListName[0]); //default list
const taskItems = filtered.map((todo) => {
return (
<Task
id={todo.id}
name={todo.name}
completed={todo.completed}
key={todo.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
);
});
const taskNoun = taskList.length !== 1 ? "tasks" : "task";
const headingText = `${taskList.length} ${taskNoun} remaining`;
function toggleTaskCompleted(id) {
const updatedTasks = taskList.map((todo) => {
if (id === todo.id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
setTaskList(updatedTasks);
}
function addTask(name) {
const newTask = { id: nanoid(), name: name, completed: false };
setTaskList([...taskList, newTask]);
}
function deleteTask(id) {
const remTasks = taskList.filter((todo) => id !== todo.id);
setTaskList(remTasks);
}
function editTask(id, newName) {
const editTaskList = taskList.map((todo) => {
if (id === todo.id) {
return { ...todo, name: newName };
}
return todo;
});
setTaskList(editTaskList);
}
return (
<div className="App">
<Header />
<AddTask addTask={addTask} />
<div>
<div className="task--list-btn">
<Filterbtns
taskList={taskList}
setFiltered={setFiltered}
filteredListName={filteredListName}
activeList={activeList}
setActiveList={setActiveList}
/>
<div className="task--lst">
<h2>TASKS</h2>
<h3>{headingText}</h3>
{taskItems}
</div>
</div>
<div>No task Available</div>
</div>
</div>
);
}
export default App
Filterbtns.js
import React from "react";
export default function Filterbtns(props) {
React.useEffect(() => {
if (props.activeList) {
props.setActiveList(props.filteredListName[0]);
console.log("try");
return;
}
const filtered = props.taskList.filter((todo) =>
todo.includes(props.activeList)
);
props.setFiltered(filtered);
}, [props.activeList]);
return (
<div className="task--btns">
<button
className="all-tasks inputs"
onClick={() => props.setActiveList(props.FilterbtnsfilteredListName[0])}
>
ALL
</button>
<br />
<button
className="active-tasks inputs"
onClick={() => props.setActiveList(props.filteredListName[1])}
>
ACTIVE
</button>
<br />
<button
className="completed-tasks inputs"
onClick={() => props.setActiveList(props.filteredListName[2])}
>
COMPLETED
</button>
</div>
);
}
I've not checked but from what it looks like React.useEffect is redundant inside Filterbtns and you need to pass down FilterbtnsfilteredListName to Filterbtns as props like this:
<Filterbtns
taskList={taskList}
setFiltered={setFiltered}
filteredListName={filteredListName}
activeList={activeList}
setActiveList={setActiveList}
FilterbtnsfilteredListName={filteredListName} // you forgot this
/>
Although if I can change the logic a bit, a better composition would be:
const FILTER_MAP = {
All: () => true,
Active: (todo) => !todo.completed,
Completed: (todo) => todo.completed
};
const FILTER_NAMES = Object.keys(FILTER_MAP); //keys
export default function App() {
const [taskList, setTaskList] = useState(data);
const [currentFilter, setCurrentFilter] = useState(FILTER_NAMES[0])
const filtered = taskList.filter(FILTER_MAP[currentFilter])
const taskItems = filtered.map((todo) => {
...
});
...
return (
<div className="App">
<Header />
<AddTask addTask={addTask} />
<div>
<div className="task--list-btn">
{/* IMPORTANT: FilterButton new API */}
<FilterButton
filterNames={FILTER_NAMES}
onFilter={setCurrentFilter}
/>
<div className="task--lst">
<h2>TASKS</h2>
<h3>{headingText}</h3>
{taskItems}
</div>
</div>
<div>No task Available</div>
</div>
</div>
);
}
function FilterButton(props) {
return (
<div className="task--btns">
{props.filterNames.map((filterName) => {
return <button
className={`${filterName}-tasks inputs`}
onClick={() => props.onFilter(filterName)}
>
{filterName}
</button>
})}
</div>
)
}
Happy React journey! you are doing great.
I am making a calculator using react.
Every time I press a number button, the whole application re-renders, instead of the <Display />.
To prevent it, I tried 2 different approaches for App, But neither of them worked.
Here is the sandbox link.
Any help would be appreciated.
Put clickHandler inside of useCallback()
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = useCallback(
(val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
},
[screen]
);
return (
<div className="App">
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</div>
);
};
Put Display component inside of React.memo
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
const displayComponent = () => {
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
};
const MemoizedComponent = React.memo(displayComponent);
return (
<div className="App">
<MemoizedComponent />
</div>
);
};
And here's the ButtonList & Button component.
export const ButtonList = ({ clickHandler }) => {
const arr = [...Array.from(Array(10).keys()).reverse(), "AC"];
return (
<div className="buttons">
<div className="numbersWrapper">
{arr.map((item) => (
<Button
key={item}
clickHandler={clickHandler}
value={item.toString()}
/>
))}
</div>
</div>
);
};
export const Button = ({ value, clickHandler }) => {
return (
<button
name={value}
onClick={() => {
clickHandler(value); //where the clickEvent happens
}}
>
{value}
</button>
);
};
If you don't want a component re-render,You would have to define the click handler in another component that you would like to re-render.
So do it like this:
const App = () => {
console.log("render");
return (
<div className="App">
<childComponent />
</div>
);
};
export const childComponent = () => {
const [screen, setScreen] = useState("0");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
}
This way you prevent a particular component from re-rendering. But note that if you update a state or do anything from which causes re-renders from the parent component, It would equally re-render the child component.
I am working where I want to automatically collapse the menu when other one opens.Now I already tried making separate useState and passing values but it doesn't work.I am sharing the before and after code with you.
In Dashboard.jsx I am maping through the menus and passing it to MenuCards.jsx as props, now if A menu is clicked hence is Expandable, it is again passed to getExpandableMenu and the menu is exapanded.
The end goal I want is one menu to open and if the other menu is clicked the first one to close first.
Before code -
// Dashboard.tsx
setMenuList([
{
title: "Thermal Comfort",
icon: thermal,
decorator: new ManekinDecorator(IModelApp.viewManager.selectedView!),
tooltip: "Thermal Comfort",
},
{
title: "Surface Plots",
icon: surfacePlot,
decorator: null,
tooltip: "Surface Plots",
},
{
title: "Contour Plots",
icon: contour,
decorator: null,
tooltip: "Contour Plots",
},
{
title: "Comfort Cloud",
icon: comfortCloud,
decorator: new ComfortDecorator(),
tooltip: "Comfort Cloud",
},
{
title: "Flowlines",
icon: flowline,
//decorator: getFlowLineDecorator(),
decorator: null,
tooltip: "FlowLines",
},
]);
} else {
setMenuList([]);
setIsDropDownVisibal(false);
}
}, [viewPort]);
<div className="menu">
<div style={{ overflowY: "scroll", width: "inherit" }}>
{menuList.map((menu) => (
<MenuCard
menu={menu.title}
icon={menu.icon}
decorator={menu.decorator}
tooltip={menu.tooltip}
/>
))}
</div>
</div>
// MenuCard.tsx
const MenuCard = (props: any) => {
const [toggle, setToggle] = React.useState(true);
const [expandOption, setExpandOption] = React.useState(false);
const onClick = () => {
if (props.decorator !== null) {
if (toggle) {
IModelApp.viewManager.decorators.forEach((decorator) => {
IModelApp.viewManager.dropDecorator(decorator);
});
IModelApp.viewManager.addDecorator(props.decorator);
} else IModelApp.viewManager.dropDecorator(props.decorator);
}
setToggle(!toggle);
setExpandOption(!expandOption);
};
const { menu, icon, tooltip } = props;
return (
<div>
<div className="mainMenu" onClick={onClick}>
<div className="icon">
<img src={icon} alt="icon" className="menuIcon" />
</div>
<div className="menuTitle">
<p className="title">{menu}</p>
</div>
<InfoTooltip tooltipText={tooltip} />
</div>
{expandOption && (
<GetExpandedMenu menuName={menu} decorator={props.decorator} />
)}
</div>
);
};
export default MenuCard;
// GetExpandedMenu.tsx
export default function GetExpandedMenu(props:any){
const {menuName} = props
const surfacePlotManager= new SurfacePlotManager()
const contourPlotManager=new ContourPlotManager();
switch(menuName) {
case 'Surface Plots':
return <SurfacePlot plotManager={surfacePlotManager}/>
case 'Thermal Comfort':
return <ThermalComfortMenu decorator={props.decorator}/>
case 'Flowlines':
return <FlowLines />
case "Contour Plots":
return <ContourPlot plotManager={contourPlotManager}/>;
case 'Comfort Cloud':
return <ComfortCloud/>
default:
return <p>expanded options</p>;
}
}
Here is what I tried doing which failed.
// MenuCard.tsx
// tried making separate useState for every menu
/* eslint-disable eqeqeq */
import React from "react";
import "./MenuCard.scss";
const MenuCard = (props: any) => {
const [toggle, setToggle] = React.useState(true);
const [expandOption, setExpandOption] = React.useState(false);
const [ThermalExpandOption, setThermalExpandOption] = React.useState(false);
const [SurfaceExpandOption, setSurfaceExpandOption] = React.useState(false);
const [ContourExpandOption, setContourExpandOption] = React.useState(false);
const [CloudExpandOption, setCloudExpandOption] = React.useState(false);
const [FlowlinesExpandOption, setFlowlinesExpandOption] = React.useState(false);
const onClick = () => {
console.log("Menu clicked is", props.id);
if (props.id == 0) {
setThermalExpandOption(!ThermalExpandOption);
setSurfaceExpandOption(false);
setContourExpandOption(false);
setCloudExpandOption(false);
setFlowlinesExpandOption(false);
} else if (props.id == 1) {
setThermalExpandOption(false);
setSurfaceExpandOption(!SurfaceExpandOption);
setContourExpandOption(false);
setCloudExpandOption(false);
setFlowlinesExpandOption(false);
} else if (props.id == 2) {
setThermalExpandOption(false);
setSurfaceExpandOption(false);
setContourExpandOption(!CloudExpandOption);
setCloudExpandOption(false);
setFlowlinesExpandOption(false);
} else if (props.id == 3) {
setThermalExpandOption(false);
setSurfaceExpandOption(false);
setContourExpandOption(false);
setCloudExpandOption(!CloudExpandOption);
setFlowlinesExpandOption(false);
} if (props.id == 4) {
setThermalExpandOption(false);
setSurfaceExpandOption(false);
setContourExpandOption(false);
setCloudExpandOption(false);
setFlowlinesExpandOption(!FlowlinesExpandOption);
}
if (props.decorator !== null) {
if (toggle) {
IModelApp.viewManager.decorators.forEach((decorator) => {
IModelApp.viewManager.dropDecorator(decorator);
});
IModelApp.viewManager.addDecorator(props.decorator);
} else IModelApp.viewManager.dropDecorator(props.decorator);
}
setToggle(!toggle);
setExpandOption(!expandOption);
};
const { menu, icon, tooltip } = props;
return (
<div>
<div className="mainMenu" onClick={onClick}>
<div className="icon">
<img src={icon} alt="icon" className="menuIcon" />
</div>
<div className="menuTitle">
<p className="title">{menu}</p>
</div>
<InfoTooltip tooltipText={tooltip} />
</div>
{ ThermalExpandOption && (
<GetExpandedMenu menuName={menu} decorator={props.decorator} />
)}
{ SurfaceExpandOption && (
<GetExpandedMenu menuName={menu} decorator={props.decorator} />
)}
{ ContourExpandOption && (
<GetExpandedMenu menuName={menu} decorator={props.decorator} />
)}
{ CloudExpandOption && (
<GetExpandedMenu menuName={menu} decorator={props.decorator} />
)}
{ FlowlinesExpandOption && (
<GetExpandedMenu menuName={menu} decorator={props.decorator} />
)}
</div>
);
};
export default MenuCard;
Lift state up.
If state of your item depends on state of menu item next to it, you can refactor both to depend on the state from "menu" container. Something like "openedItem" state.
You can find detailed instructions in React docs here:
https://reactjs.org/docs/lifting-state-up.html ,
with examples.
I am getting images from unsplash api, and I am able to render them on the page. I am implementing a lightbox to be able to render the high res pic of the images in the page. I dont know how to go forward, I will show some code.
const ImageList = ({ image, isLoaded }) => {
// const [imageIndex, setImageIndex] = useState(0);
const [isOpen, setIsOpen] = useState('false');
if (isLoaded) {
return (
<div className="spinner">
<ReactLoading type="spin" color="blue" />
</div>
);
}
const onClickHandler = () => {
setIsOpen(true);
};
const imgs = image.map(img => (
<img
key={img.id}
src={img.urls.small}
onClick={onClickHandler}
/>
));
if (imgs.length === 0) {
return (
<p>No images</p>
);
}
if (isOpen === true) {
return (
<Lightbox
onCloseRequest={() => setIsOpen(false)}
mainSrc=
/>
);
}
return (
<React.Fragment>
{imgs}
</React.Fragment>
);
};
export default ImageList;