How to call API inside switch case - reactjs

I have created a tab component where I have three tabs each tab will contain data from different end points. I have planned to hit API's using switch case since I am having different end points. I have created a onChange function where I used to call three different end points, unfortunately I couldn't able to achieve the result, I am getting error stating that useEffect cannot be used inside onChange. Could anyone guide me to achieve the result. Thanks in advance.
API'S I am using are :https://reqres.in/api/users?page=1, https://reqres.in/api/users?page=2, https://reqres.in/api/users?page=3
import React, { useState, useEffect } from "react";
import Paper from "#material-ui/core/Paper";
import Tab from "#material-ui/core/Tab";
import Tabs from "#material-ui/core/Tabs";
const Tabb = () => {
const [profileData, setProfileData] = useState(0);
useEffect(() => {
fetch("https://reqres.in/api/users?page=1")
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
});
const handleChange = (e, value) => {
switch (value) {
case 0:
break;
case 1:
break;
case 2:
break;
default:
break;
}
setProfileData(value);
};
return (
<div style={{ marginLeft: "40%" }}>
<h2>Tbas in React JS</h2>
<Paper square>
<Tabs
value={profileData}
textColor="primary"
indicatorColor="primary"
onChange={handleChange}
>
<Tab label="Tab One" />
<Tab label="Tab Two" />
<Tab label="Tab Three" />
</Tabs>
{/* <h3>Tab NO: {value} clicked!</h3> */}
</Paper>
</div>
);
};

You have a single API with a changing page parameter. Whenever you set the page (tab changed), update the state with the number of the page.
Set the page as the useEffect() dependency, so an API call would be issued whenever the page changes. Add the current page to the base query url to the current url.
Demo - choose a tab in the select menu:
const { useState, useEffect } = React;
const Paper = 'div';
const Tabs = 'select';
const Tab = ({ label, ...rest }) => (<option {...rest}>{label}</option>);
const Demo = () => {
const [page, setPage] = useState(0);
useEffect(() => {
const url = `https://reqres.in/api/users?page=${page}`;
fetch(url)
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
}, [page]);
const handleChange = e => {
setPage(+e.target.value);
};
return (
<Paper>
<Tabs onChange={handleChange}>
<Tab label="Tab One" value={1} />
<Tab label="Tab Two" value={2} />
<Tab label="Tab Three" value={3} />
</Tabs>
{/* <h3>Tab NO: {value} clicked!</h3> */}
</Paper>
);
};
ReactDOM.render(
<Demo />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>

If I understand your question/issue you want to conditionally fetch some data and update state via a switch statement.
Issues
The useEffect without a dependency will be called every render cycle. This is likely not what you want.
You can't conditionally call hooks in loops and callbacks, they must only be called from react functions and other custom hooks.
Solution
I don't think you need the useEffect hook at all, you can define a function to handle the fetch, or better, just use a single fetch in the onClick handler.
const handleChange = (e, value) => {
let url;
switch (value) {
case 0:
default:
url = "https://reqres.in/api/users?page=1";
break;
case 1:
url = "https://reqres.in/api/users?page=2";
break;
case 2:
url = "https://reqres.in/api/users?page=3";
break;
}
fetch(url)
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
setProfileData(value);
};
You can simplify this a bit more, the switch cases are basically just adding 1 to the value and forming the url. Just use a string template to form the request URL.
const handleChange = (e, value) => {
fetch(`https://reqres.in/api/users?page=${value + 1}`)
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
setProfileData(value);
};

Related

Using state and props between React components

This is my project for business card app.
I have a problem with using state and props between components.
Component tree looks like this.
Editor <- CardEditForm <- ImageFileInput
The url state is in the Editor component. There is a function to update state and give it as props to child components. When I upload an image on ImageFileInput component, url data goes up until the editor component and then using setUrl to url be updated. And then I gave url to CardEditorForm component.
The problem is this, In cardEditorForm, when it comes to using url props, I can't get the updated url. Only gets the initial state. I really need to get an updated url. I also tried to use setTimeout() to get the updated url. But it doesn't work either. What can I do?..
It's my first time to ask a question on stack overflow. Thank you for helping the newb.
Here is the code.
editor.jsx
const Editor = ({ cards, deleteCard, createOrUpdateCard }) => {
const [url, setUrl] = useState('');
const updateUrl = (src) => {
setUrl(src);
};
return (
<section className={styles.editor}>
<h1 className={styles.title}>Card Maker</h1>
{Object.keys(cards).map((key) => (
<CardEditForm
key={key}
card={cards[key]}
onDelete={deleteCard}
onUpdate={createOrUpdateCard}
updateUrl={updateUrl}
url={url}
/>
))}
<CardAddForm onAdd={createOrUpdateCard} updateUrl={updateUrl} url={url} />
</section>
);
};
card_edit_form.jsx
const CardEditForm = ({ card, onDelete, onUpdate, updateUrl, url }) => {
// ...
const changeUrl = () => {
setTimeout(() => {
const newCard = {
...card,
fileURL: url,
};
onUpdate(newCard);
}, 4000);
};
return (
<form className={styles.form}>
// ...
<div className={styles.fileInput}>
<ImageFileInput updateCard={changeUrl} updateUrl={updateUrl} />
</div>
// ...
</form>
);
};
export default CardEditForm;
image_file_input.jsx
const ImageFileInput = ({ updateUrl, updateCard }) => {
const [image, setImage] = useState('');
const upload = new Upload();
const onUpload = (e) => {
e.preventDefault();
upload.uploadImage(image).then((data) => updateUrl(data));
updateCard(e);
};
return (
<div>
<input type="file" onChange={(e) => setImage(e.target.files[0])} />
<button name="fileURL" onClick={onUpload}>
image
</button>
</div>
);
};

URL changes set by react-router-dom 'useSearchParams' are not maintained when set by mui 'Tabs' component's onChange callback

I would like to set a tab navigation component based on the search parameter named tab.
If my url is example.com?tab=test2, I would like my navigation bar to display the test2 item as selected.
I'm using mui Tabs component for this.
I'm able to successfully set my Tab state by getting the search parameter with searchParams.get('tab'), however, when I set the tab property, it's only reflected for a moment, before being overwritten with a null state (and so the URL is changed to have no search parameters).
I've tried to add a null checker to set a default search parameter, but it isn't "keeping."
What's very strange is that if I simply set search params using plain buttons, the changes "keep."
I've created a codesandbox minimal implementation to demonstrate, however, in short, my change function, that gets invoked both within the mui Tabs component's onChange callback as well as my simple button onClick callbacks, looks like this:
const handleChange = (event: SyntheticEvent, newValue: string) => {
searchParams.set("tab", newValue);
setSearchParams(searchParams);
};
And the value is initially set upon component instantiation (I believe based on console log experiments) with:
const PartsTabNav = () => {
let [searchParams, setSearchParams] = useSearchParams();
let queryTab = searchParams.get("tab");
Why do my buttons change the URL, but not the MUI Tabs component's onChange, even though both invoke the same function?
EDIT: It seems it might actually have something to do with the <Link> component based on my debugging.
The full example code is below:
import { Grid, Tab, Tabs } from "#mui/material";
import { useTheme } from "#mui/material/styles";
import { SyntheticEvent, useEffect } from "react";
import { Link, useSearchParams, Routes, Route } from "react-router-dom";
const tabOptions = [
{
label: "Test1",
route: "test1"
},
{
label: "Test2",
route: "test2"
}
];
const PartsTabNav = () => {
let [searchParams, setSearchParams] = useSearchParams();
let queryTab = searchParams.get("tab");
queryTab = queryTab ? queryTab : "test1";
useEffect(() => {
if (!queryTab) {
searchParams.set("tab", "test1");
setSearchParams(searchParams);
}
}, [searchParams, setSearchParams, queryTab]);
const handleChange = (event: SyntheticEvent, newValue: string) => {
searchParams.set("tab", newValue);
setSearchParams(searchParams);
};
return (
<div>
<Grid container spacing={1}>
<Grid item xs={12}>
<ul>
<li>
<button
onClick={(e) => {
handleChange(e, "test1");
}}
>
one
</button>
</li>
<li>
<button
onClick={(e) => {
handleChange(e, "test2");
}}
>
two
</button>
</li>
</ul>
<Tabs
value={queryTab}
indicatorColor="primary"
textColor="primary"
aria-label="Tabs"
onChange={handleChange}
>
{tabOptions.map((tab, index) => (
<Tab
key={index}
component={Link}
to="#"
label={tab.label}
value={tab.route}
/>
))}
</Tabs>
</Grid>
</Grid>
</div>
);
};
export default function App() {
return (
<div>
<Routes>
<Route path="/" element={<PartsTabNav />} />
</Routes>
</div>
);
}
The issue was coming from using Link (from react-router) as a component prop for Tab. There was some default click handling that was happening that was causing a page refresh. Changing to the below, fixed the issue.
{tabOptions.map((tab, index) => (
<Tab
key={index}
icon={tab.icon}
label={tab.label}
value={tab.route}
/>
))}

How to get single value in react-select dropdown

I am trying to create a multi-select dropdown indicator (the second element shown here) using react-select.
The purpose is to show all blog post categories on a blog page, and then to only render the blog posts that are selected in the dropdown indicator.
The tags are extracted from their posts based on a GraphQL query and stored in useState variables "tags" and "renderedPosts".
How do I simply get a value from the dropdown when a category is added or removed? Reading the react-select API, I get this:
getValue () => ReadonlyArray<...>
I don't know how to use that, VS Code simply screams when I try add an arrow function as an attribute in the Select.
I understand there is supposed to be a "value" by default on the Select but if I try to use it I get undefined.
I don't know if mapping to the default value is a problem or if it has anything to do with the ContextProvider (which was necessary). There are other attributes I cannot get to work either, like maxMenuHeight (which is supposed to take a number).
Allow me to share my code (you can probably ignore the useEffect):
export default function Blog({ data }) {
const { posts } = data.blog
const [tags, setTags] = useState([])
const [renderedPosts, setRenderedPosts] = useState([])
// Context necessary to render default options in react-select
const TagsContext = createContext();
// setting all tags (for dropdown-indicator) and rendered posts below the dropdown (initially these two will be the same)
useEffect(() => {
const arr = [];
data.blog.posts.map(post => {
post.frontmatter.tags.map(tag => {
if (!arr.some(index => index.value === tag)) {
arr.push({ value: tag, label: tag })
}
})
});
setTags([arr]);
setRenderedPosts([arr]);
}, []);
function changeRenderedPosts(value???){
setRenderedPosts(value???)
}
return (
<Layout>
<div>
<h1>My blog posts</h1>
<TagsContext.Provider value={{ tags }}>
<Select
defaultValue={tags.map(tag => tag) }
isMulti
name="tags"
options={tags}
className="basic-multi-select"
classNamePrefix="select"
// HOW DO I PASS THE VALUE OF THE ADDED/DELETED OPTION?
onChange={() => changeRenderedPosts(value???)}
maxMenuHeight= {1}
/>
</TagsContext.Provider>
// posts to be rendered based on renderedPosts value
{posts.map(post => {
EDIT: The closest I have now come to a solution is the following:
function changeRenderedTags(options){
console.log(options) //logs the remaining options
setRenderedTags(options) //blocks the change of the dropdown elements
}
return (
<Layout>
<div>
<h1>My blog posts</h1>
<TagsContext.Provider value={{ tags }}>
<Select
...
onChange={(tagOptions) => changeRenderedTags(tagOptions)}
I click to delete one option from the dropdown and I get the other two options in "tagOptions". But then if I try to change "renderedTags", the update of the state is blocked. I find this inexplicable as "setRenderedTags" has nothing to do with the rendering of the dropdown or its data!
with isMulti option true, you get array of options(here it's tags) from onChange callback. so I guess you could just set new tags and render filtered posts depending on the selected tags like below?
const renderedPosts = posts.filter(post => post.tags.some(tag => tags.includes(tag)))
...
onChange={selectedTags => {
setTags(selectedTags ? selectedTags.map(option => option.value) : [])
}}
...
I finally solved it - I don't know if it is the best solution but it really works! Sharing it here in case anyone else is in the exact same situation, or if you are just curious. Constructive criticism is welcome.
export default function Blog({ data }) {
const { posts } = data.blog
const [tags, setTags] = useState([])
const [renderedTags, setRenderedTags] = useState([])
const TagsContext = createContext();
useEffect(() => {
const arr = [];
posts.map(post => {
post.frontmatter.tags.map(tag => {
if (!arr.some(index => index.value === tag)) {
arr.push({ value: tag, label: tag })
}
})
});
setTags([...arr]);
}, [posts]);
useEffect(() => {
setRenderedTags([...tags]);
}, [tags])
return (
<Layout>
<div>
<h1>My blog posts</h1>
<TagsContext.Provider value={{ tags }}>
<Select
defaultValue={tags}
isMulti
name="tags"
options={tags}
className="basic-multi-select"
classNamePrefix="select"
onChange={(tagOptions) => setRenderedTags(tagOptions ? tagOptions.map(option => option) : [])}
value={renderedTags}
/>
</TagsContext.Provider>
{posts.map(post =>
(post.frontmatter.tags.some(i => renderedTags.find(j => j.value === i))) ?
<article key={post.id}>
<Link to={post.fields.slug}>
<h2>{post.frontmatter.title}</h2>
<p>{post.frontmatter.introduction}</p>
</Link>
<small>
{post.frontmatter.author}, {post.frontmatter.date}
</small>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
: null
)}
</div>
</Layout>
)
}

React: How to prevent menu component from being remounted when switching pages

Let's say that we have a React app with two pages A and B using a shared menu component Menu.
Our app renders either page A or page B, like the example below:
const Menu = (props) => {
React.useEffect(()=>{
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button onClick={() => props.onClick('a')}>A</button>
<button onClick={() => props.onClick('b')}>B</button>
</div>
);
}
const PageA = (props) => {
const .. = useSomeHooksUsedByPageA();
return (
<div>
<Menu {...somePropsFromPageA} />
<div>Content of page A</div>
</div>
);
}
const PageB = (props) => (
const .. = useSomeHooksUsedByPageB();
<div>
<Menu {...somePropsFromPageB} />
<div>Content of page B</div>
</div>
);
const App = () => {
const [pageKey, setPageKey] = React.useState("a");
switch (pageKey)
{
case "a":
return <PageA key="1" onClick={setPageKey} />;
case "b":
return <PageB key="1" onClick={setPageKey} />;
}
return "true"
}
Now, every time we switch pages (from A to B, or B to A), the menu is remounted and a message is printed to the console.
Using this component hierarchy where the menu receives props from the page, is there any way to tell React not to remount the menu when we switch pages?
(A typical use-case could be that the menu has a scroll, and we want to keep the scroll position when navigating different pages.)
Help is greatly appreciated!
One potential solution for this problem is to move <Menu/> into the <App/> component, and render each page after the menu.
This provides a couple of benefits:
The Menu won't be re-rendered whenever the page changes.
The onClick function does not need to be passed through props on each page just to provide it to the <Menu/> component nested within.
const Menu = (props) => {
React.useEffect(() => {
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button onClick={() => props.onClick("a")}>A</button>
<button onClick={() => props.onClick("b")}>B</button>
</div>
);
};
const PageA = () => (
<div>
<div>Content of page A</div>
</div>
);
const PageB = () => (
<div>
<div>Content of page B</div>
</div>
);
const App = () => {
const [pageKey, setPageKey] = React.useState("a");
let page;
switch (pageKey) {
case "b":
page = <PageB key="2" />;
break;
default:
page = <PageA key="3" />;
break;
}
return (
<>
<Menu onClick={setPageKey} />
{page}
</>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
Edit
Further to #glingt's comment regarding the hierarchy and how this needs to function, Context might be a good candidate for the use case. If pages need to update the <Menu/> component's props, then using context to manage state between the menu and pages might be a better solution in terms of architecture. Instead of rendering many <Menu/> components inside of each child, only one <Menu/> can be rendered higher up in the tree. This results in the component mounting once rather than many times with each child. Effectively, context manages the state of the menu, and provides methods to update state to any children under the provider. In this case, both child pages and the menu can update and respond to state updates.
import "./styles.css";
import React, { useContext, useMemo, useState } from "react";
// Create an instance of context so we are able to update the menu from lower in the tree
const menuContext = React.createContext({});
// Add state to the context provider. Wrap props earlier in the tree with this component.
const MenuContext = ({ children }) => {
const [pageKey, setPageKey] = useState("a");
const value = useMemo(() => ({ pageKey, setPageKey }), [pageKey]);
return <menuContext.Provider value={value}>{children}</menuContext.Provider>;
};
// The menu component which will:
// 1. Update the menuContext when the user selects a new pageKey
// 2. Respond to updates made to the pageKey by other components (in this case pages)
const Menu = () => {
const { pageKey, setPageKey } = useContext(menuContext);
React.useEffect(() => {
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button
onClick={() => setPageKey("a")}
style={{ color: pageKey === "a" ? "blue" : "red" }}
>
A
</button>
<button
onClick={() => setPageKey("b")}
style={{ color: pageKey === "b" ? "blue" : "red" }}
>
B
</button>
</div>
);
};
// In each page, we are able to update a value that is consumed by the menu using setPageKey
const PageA = () => {
const { setPageKey } = useContext(menuContext);
return (
<div>
<div>Content of page A</div>
<button onClick={() => setPageKey("b")}>Go to page B</button>
</div>
);
};
const PageB = () => {
const { setPageKey } = useContext(menuContext);
return (
<div>
<div>Content of page B</div>
<button onClick={() => setPageKey("a")}>Go to page A</button>
</div>
);
};
const PageComponent = () => {
const { pageKey } = useContext(menuContext);
switch (pageKey) {
case "b":
return <PageB key="2" />;
default:
return <PageA key="1" />;
}
};
const App = () => (
<MenuContext>
<Menu />
<PageComponent />
</MenuContext>
);
ReactDOM.render(<App />, document.querySelector("#app"))

React Material Tabs indicator not showing on first load

I have a simple Tabs setup with React Material UI (https://material-ui.com/components/tabs/) where the path value is set dynamically
export const Subnav: React.FC<Props> = ({ routes = [] }) => {
const { pathname } = useLocation();
const { push } = useHistory();
const handleChange = (e: ChangeEvent<{}>, path: string) => push(path);
return (
<Tabs
indicatorColor="primary"
onChange={handleChange}
scrollButtons="auto"
textColor="primary"
value={pathname}
variant="scrollable"
>
{routes.map(r => (
<Tab label={r.name} value={r.path} />
))}
</Tabs>
);
};
When I first load a page / navigate to one of the tab routes, the correct tab is selected, but the indicator is not shown. In order for the indicator to be shown I have to click the same tab again or select another.
For this, there is one more way to handle this.
In React material UI there is component <Grow/>
you can warp <Tabs/> within <Grow/> and <Grow/> will correct the indicator position.
Following is the example:
<Grow in={true}>
<Tabs
action={ref => ref.updateIndicator()}
>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
<Tabs>
</Grow>
This was resolved via https://github.com/mui-org/material-ui/issues/20527
You need to manually trigger the updateIndicator method. Easiest way to do this, is to call a resize event (which triggers the method)
useEffect(() => {
window.dispatchEvent(new CustomEvent("resize"));
}, []);
Alternatively add a ref to the actions prop and call the method directly. Still seems like a non-ideal solution, but it is what the maintainer provided.
My solution based on above ones but a bit different. The difference is that I update the indicator via a deferred call (setTimeout) with 400ms delay. Don't know real reason.
Below my definition of Mui-Tabs wrapper
import * as React from 'react';
import MaterialTabs, { TabsActions } from '#material-ui/core/Tabs';
import { withStyles } from '#material-ui/core';
import { IProps } from './types';
import styles from './styles';
class Tabs extends React.PureComponent<IProps> {
tabsActions: React.RefObject<TabsActions>;
constructor(props) {
super(props);
this.tabsActions = React.createRef();
}
/**
* Read more about this here
* - https://github.com/mui-org/material-ui/issues/9337#issuecomment-413789329
*/
componentDidMount(): void {
setTimeout(() => {
if (this.tabsActions.current) {
this.tabsActions.current.updateIndicator();
}
}, 400); // started working only with this timing
}
render(): JSX.Element {
return (
<MaterialTabs
action={this.tabsActions}
indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
{...this.props}
/>
);
}
}
export default withStyles(styles)(Tabs);
export const Subnav: React.FC<Props> = ({ routes = [], render }) => {
const { pathname } = useLocation();
const { push } = useHistory();
const handleChange = (e: ChangeEvent<{}>, path: string) => push(path);
const ref = useRef();
useEffect(() => {ref.current.updateIndicator()}, [pathname, render])
return (
<Tabs
action={ref}
indicatorColor="primary"
onChange={handleChange}
scrollButtons="auto"
textColor="primary"
value={pathname}
variant="scrollable"
>
{routes.map(r => (
<Tab label={r.name} value={r.path} />
))}
</Tabs>
);
};
Put into props.render your anything dynamically value

Resources