I am conditionally rendering components(screens) as user will be guided through different screens. I am trying to implement a functionality where user can use back and forward buttons of browser to navigate between these different screens or components. I am struggling to implement this. Any suggestions will be appreciated.
link to sandbox:
https://codesandbox.io/s/history-push-and-pop-state-listening-forked-1keiz?file=/src/App.js
const [show1, setShow1] = useState(true);
const [show2, setShow2] = useState(false);
const [show3, setShow3] = useState(false);
let stateForHistory = {
show1: false,
show2: false,
show3: false
};
const handleClick = () => {
setShow1(false);
setShow2(true);
if(show2)
setShow2(false)
setShow3(true)
};
//saving state onmount
useEffect(() => {
window.history.pushState(stateForHistory, "", "");
}, []);
useEffect(() => {
window.addEventListener("popstate", (e) => {
let { show1, show2, show3 } = e.state || {};
if (!show1 && show2) {
setShow1(true);
setShow2(false);
}
});
return () => {
window.removeEventListener("popstate", null);
};
});
return (
<div className="App">
<div id="screen-1">
{show1 && <Screen1 />}
</div>
<div id="screen-2">
{show2 && <Screen2 />}
</div>
<div id="screen-3">
{show3 && <Screen3 />}
</div>
<button onClick={handleClick} id="btn">
Next
</button>
</div>
);
}
I would suggest using a routing package to handle the navigation aspect, your code can focus on the screen. What you are describing sounds similar to a stepper.
index
Wrap the App in a router.
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Router>
<App />
</Router>,
rootElement
);
App
Define a route with a path parameter to match a "step" or screen.
function App() {
return (
<div className="App">
<Switch>
<Route path="/step/:step">
<Stepper />
</Route>
<Redirect to="/step/1" />
</Switch>
</div>
);
}
Create a screen stepper component to listen for changes in the route, specifically to the step parameter, and conditionally render the screen.
const ScreenStepper = () => {
const history = useHistory();
const { step } = useParams();
const { path } = useRouteMatch();
const nextStep = (next) => () =>
history.push(generatePath(path, { step: Number(step) + next }));
const renderStep = (step) => {
switch (step) {
case 3:
return <Screen3 />;
case 2:
return <Screen2 />;
case 1:
return <Screen1 />;
default:
}
};
return (
<>
<h1>Step: {step}</h1>
{renderStep(Number(step))}
<button disabled={Number(step) === 1} onClick={nextStep(-1)} id="btn">
Previous
</button>
<button onClick={nextStep(1)} id="btn">
Next
</button>
</>
);
};
You can expand/customize upon this to limit the screen count, or render the screen routes from an array, etc...
Related
I'm basically trying to show some data in one of my components. The data is passed from my main page but I can't get it to work using useLocation.
I'm getting the data from my firebase db.
My main page is a job board and I want users to be able to click on the job card and go to a new page with all the details of that job.
I see on the console that I get the data but I can't seem to display it in my component/page. I get undefined when doing console.log
See below for more details:
Jobs.js
import { Link, useNavigate } from "react-router-dom";
export default () => {
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
return (
<div>
{jobs.map(job => {
return (
<div key={job.id}>
//My attempt using Link
<Link
to={`/view-contact-details/${job.id}`}
state={{jobs}}
>
<button>View</button>
</Link>
//My attempt using useNavigate
<button onClick={() => {
navigate(`/view-contact-details/${job.id}`, { state:{ jobs } });
}}
>
Go To Job details
</button>
</div>
);
})}
</div>
);
};
App.js
import React from "react";
import Jobs from "./pages/jobs"
import Form from "./pages/form"
import { Routes, Route } from "react-router-dom"
import ViewUserDetails from "./components/Job/ViewJobDetails";
export default () => {
return (
<div className="App">
<Routes>
<Route exact path='/' element={<Jobs/>} />
<Route exact path='/form' element={<Form/>} />
<Route
exact
path="/view-contact-details/:id"
element={<ViewJobDetails/>}
/>
</Routes>
</div>
);
};
ViewJobDetails.js
import React from "react";
import { useLocation, } from "react-router-dom";
export default (props) => {
const location = useLocation();
console.log(location); // shows jobs on the page yet can't display
//I also tried
//const {state} = location
//{state.job.description}
return (
<>
<div>
<div>
<div>
<strong>Description:</strong>
{location.state.job.description} //doesn't work
{location.job.description} //doesn't work
{location.state.description} //doesn't work
</div>
<div>
<strong>Title:</strong>
</div>
</div>
</div>
</>
);
};
console.log output
The passed state jobs is an array. In the component you are accessing a job property that is undefined.
Access the correct state.jobs array and map it to JSX.
Example:
export default (props) => {
const { state } = useLocation();
const { jobs } = state || []; // <-- access state.jobs
return (
<div>
{jobs.map(job => ( // <-- map jobs array
<React.Fragment key={job.id}>
<div>
<strong>Description:</strong>
{job.description}
</div>
<div>
<strong>Title:</strong>
{job.title}
</div>
</React.Fragment>
))}
</div>
);
};
If on the offhand chance you meant to pass only a single job, i.e. the current job from Jobs, then instead of passing the entire array, pass only the currently iterated job object.
export default () => {
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
return (
<div>
{jobs.map(job => {
return (
<div key={job.id}>
//My attempt using Link
<Link
to={`/view-contact-details/${job.id}`}
state={{ job }} // <-- pass current job
>
<button>View</button>
</Link>
//My attempt using useNavigate
<button
onClick={() => {
navigate(
`/view-contact-details/${job.id}`,
{ state:{ job } } // <-- pass current job
);
}}
>
Go To Job details
</button>
</div>
);
})}
</div>
);
};
Then in the JobDetals component access location.state.job.
export default (props) => {
const { state } = useLocation();
const { job } = state || {};
return (
<>
<div>
<div>
<div>
<strong>Description:</strong>
{job.description}
</div>
<div>
<strong>Title:</strong>
{job.title}
</div>
</div>
</div>
</>
);
};
I am using next.js and I get the following error: "Warning: Expected server HTML to contain a matching nav in div".
Here is my code:
export default function Member() {
const router = useRouter();
const {
isAuthenticated,
authState,
profileState
} = useContext(AuthContext);
let profileArray = profileState.savedProfile;
const redirect = () => {
if (typeof window !== "undefined") {
router.push("/");
}
};
return (
<>
{!isAuthenticated() ? (
redirect()
) : (
<>
<Navigation />
<main className="main">
{{ /* Code that uses let profileArray */ }}
</main>
</>
)}
</>
);
}
I think the error occurs because of the React.Fragment - because when I replace the React.Fragments with f.ex. div, I get a different error "Warning: Expected server HTML to contain a matching div in div".
I just don't know how to solve this issue. I already tried to use useEffect, but then I get a server error.
Any suggestions? I'm new to next.js, so any help with code improvement is appreciated! Thanks a lot!
EDIT
A similar error occurs on other pages - only the warning differs f.ex. "Warning: Expected server HTML to contain a matching div in button". I think this is related to the AvatarDropdown, which is part of the Navigation. The code:
const AvatarDropdown = () => {
const node = useRef();
const { authState, isAuthenticated, logout } = useContext(AuthContext);
const [dropdownOpen, setDropdownOpen] = useState(false);
const router = useRouter();
const member = () => {
router.push("/member");
};
const dropdownItems = [
{ title: "My Account", onClick: () => member() },
{title: "Log Out", onClick: logout},
];
const handleClick = (e) => {
if (node.current && !node.current.contains(e.target)) {
setDropdownOpen(false);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, []);
return (
<div ref={node}>
{isAuthenticated() ? (
<button
onClick={() => setDropdownOpen(!dropdownOpen)}
>
<div>
<p>{authState.name}</p>
<ExpandMoreIcon />
</div>
</button>
) : (
<Link href="/login">
<button>
<p>LOGIN</p>
</button>
</Link>
)}
{dropdownOpen && (
<div>
<DropdownContent dropdownItems={dropdownItems} />
</div>
)}
</div>
);
};
redirect does not return any html node. Any side effect code should be executed in useEffect hook.
This is recommended. Try this..
export default function Member() {
const router = useRouter();
const {
isAuthenticated,
authState,
profileState
} = useContext(AuthContext);
let profileArray = profileState.savedProfile
React.useEffect(()=> {
if(!isAuthenticated()){
router.push('/')
}
},[])
if(!isAuthenticated()){
return <p>Loading..</p>
}
return (
<>
<Navigation / >
<main className = "main">
//code that uses let profileArray
</main>
</>
);
}
U can use Redirect component from 'react-router-dom'.
I am trying to make my App.js route to my People.jsx etc.. but it is not working correctly. I hope I could fix the issue from there if I could make this work. I have been trying to do this for about 2 hours with the 20 min rule but this one I need help with. I have tried other variations but my goal is to get the,theID over to Person as well. I am thinking about using {useContext } to do that but I can't even get it to route. I wish I knew what I was doing wrong so I could correct it but other people are using different types of routers and I was confused with them even more.
I updated it with links still a no go for me any other suggestions?
App.js
import './App.css';
import { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import People from './components/People'
import Planet from './components/Planets'
import Starship from './components/Starships'
import { Router, Link } from '#reach/router';
function App() {
const [starwarsState, setStarwarsState] = useState('')
const [theID, setTheID] = useState('')
const selectedState = (e) => {
setStarwarsState(e.target.value)
}
const switchItem = () => {
switch (starwarsState) {
case 'people':
<Link path='/people/' />;
break;
case 'planets':
<Link path="/planets/" />;
break;
case 'starships':
<Link path='/starships/' />;
break;
default:
return null;
}
}
const addId = e => {
setTheID(e.target.value)
console.log(theID)
}
return (
<div className='App'>
<header className='App-header' >
Search For:
<select onChange={selectedState} className='form-control-lg bg-dark text-white'>
<option value='people' active >People</option>
<option value='planets' >Planets</option>
<option value='starships' >Starships</option>
</select>
ID:
<input type='text' onChange={addId} className='form-control-lg col-sm-1 bg-dark text-white' />
<button className='btn-lg btn-warning' onClick={switchItem} >Search Item</button>
<Router>
<People path='/people/' />
<Planet path="/planets/" />
<Starship path='/starships/' />
</Router>
</header>
{starwarsState}
</div>
)
}
export default App;
People.jsx
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { Link } from '#reach/router';
const People = props => {
const [peopleData, setpeopleData] = useState([]);
useEffect(() => {
axios.get(`https://swapi.dev/api/people/${props.theID}`)
.then(response => { setpeopleData(response.data) })
console.log(peopleData)
}, []);
return (
<div>
<span> the People have spoken</span>
<Link to='/people' />
</div>
)
}
export default People;
Issues
You aren't actually rendering the routes/links from switchItem since onClick callbacks can't return renderable UI directly to the render method.
Solution
Unconditionally render your routes all at the same time within a single Router and imperatively navigate to them in the switchItem handler.
App
...
import { Router, navigate } from "#reach/router";
...
function App() {
const [starwarsState, setStarwarsState] = useState("");
const [theID, setTheID] = useState("");
...
const switchItem = () => {
switch (starwarsState) {
case "people":
navigate("/people"); // <-- imperative navigation
break;
case "planets":
navigate("/planets");
break;
case "starships":
navigate("/starships");
break;
default:
return null;
}
};
return (
<div className="App">
<header className="App-header">
Search For:
<select
onChange={selectedState}
value={starwarsState}
className="form-control-lg bg-dark text-white"
>
<option disabled value="">
Choose Path
</option>
<option value="people">
People
</option>
<option value="planets">Planets</option>
<option value="starships">Starships</option>
</select>
ID:
<input
type="text"
onChange={addId}
className="form-control-lg col-sm-1 bg-dark text-white"
/>
<button className="btn-lg btn-warning" onClick={switchItem}>
Search Item
</button>
</header>
<Router>
<People path="/people" theID={theID} /> // <-- pass `theID` state as prop
<Planet path="/planets" />
<Starship path='/starships' />
</Router>
</div>
);
}
People
const People = ({ theID }) => {
const [peopleData, setpeopleData] = useState([]);
useEffect(() => {
axios.get(`https://swapi.dev/api/people/${theID}`)
.then(response => { setpeopleData(response.data) });
}, [theID]);
return (
<div>
<div>The ID: {theID}</div>
<span>the People have spoken</span>
</div>
);
};
Use Imperative Routing (not switch statement) with Event Handlers
Your code is using a switch statement in combination with the switchItem() function. This is not how to redirect the user imperatively (meaning, through something other than a link clicked directly by the user).
To imperatively route your users, use the navigate method.
Via Reach Router docs (link):
Sometimes you need to navigate in response to something other than the user clicking on a link. For this we have navigate. Let’s import navigate.
import {
Router,
Link,
navigate
} from "#reach/router"
In your case, the entire switch statement can be rewritten as follows:
useEffect(() => navigate(`/${starwarsState}`), [starwarsState])
useEffect will watch for changes to the starwarsState, which is either going to be 'people', 'planets', or 'starships'. Once a change occurs, it will navigate the user to the corresponding path.
Solution: Routing Only
The following solution doesn't implement axios, it focuses solely on the client-side routing logic.
I found some other issues with your code when I was working through a solution. Here is a version that I wrote that implements parameter-level routing while also making some other cleanup (refactoring the swapi categories into a config object, etc).
App.js
import React, { useState, useEffect } from 'react'
import { useWhatChanged } from "#simbathesailor/use-what-changed";
import { Router, Link, navigate } from "#reach/router";
import 'bootstrap/dist/css/bootstrap.min.css';
import { People, Person } from './components/People'
import { Planets, Planet } from './components/Planets'
import { Starships, Starship } from './components/Starships'
import './App.css';
function App() {
// destructure categories from config
const { people, planets, starships } = config.categories
// initialize state
const [starwarsState, setStarwarsState] = useState(people);
const [theID, setTheID] = useState('');
// log updates to ID and starwarsState
useWhatChanged([starwarsState, theID], 'starwarsState, theID')
// change state on input from user
const addId = e => setTheID(e.target.value)
const selectCategory = (e) => setStarwarsState(e.target.value)
// route the user based on starwarsState
useEffect(() => navigate(`/${starwarsState}`), [starwarsState])
// search swapi based on category and id
const searchSwapi = e => {
e.preventDefault()
navigate(`/${starwarsState}/${theID}`)
}
return (
<div className="App">
<header className='App-header' >
Search For:
<form onSubmit={searchSwapi}>
<select onChange={selectCategory} className='form-control-lg bg-dark text-white'>
<option value={people} >People</option>
<option value={planets} >Planets</option>
<option value={starships} >Starships</option>
</select>
ID:
<input type='text' onChange={addId} className='form-control-lg col-sm-1 bg-dark text-white' />
<button className='btn-lg btn-warning' >Search Item</button>
</form>
</header>
<Router>
<People path='/people/'>
<Person path=':personId' />
</People>
<Planets path="/planets/">
<Planet path=':planetId' />
</Planets>
<Starships path='/starships/'>
<Starship path=':starshipId' />
</Starships>
</Router>
</div>
)
}
const config = {
categories: {
people: 'people',
planets: 'planets',
starships: 'starships'
}
}
export default App;
Planets.js
import React from 'react'
export const Planets = props => {
return (
<div>
<span> the Planets have spoken</span>
{props.children}
</div>
)
}
export const Planet = props => {
return (
<div>
Planet Data
</div>
)
}
People.js
import React, { useState, useEffect } from 'react'
export const People = props => {
return (
<div>
<span> the People have spoken</span>
{props.children}
</div>
)
}
export const Person = props => {
return (
<div>
Person Data
</div>
)
}
Starships.js
import React from 'react'
export const Starships = props => {
return (
<div>
<span> the Starships have spoken</span>
{props.children}
</div>
)
}
export const Starship = props => {
return (
<div>
Starship Data
</div>
)
}
[UPDATE] Solution: Routing with API Calls
The following solution takes the code from above and refactors it using the state management pattern proposed by Leigh Halliday. The solution adds three things:
useContext for managing global state
React.memo() for memoizing AppContent component
react-query for managing remote API calls to SWAPI.
View code on GitHub
App.js
// App.js
import React, {
useState,
useEffect,
createContext,
useContext,
memo
} from 'react'
import { ReactQueryDevtools } from "react-query-devtools";
import { useWhatChanged } from "#simbathesailor/use-what-changed";
import { Router, navigate } from "#reach/router";
import 'bootstrap/dist/css/bootstrap.min.css';
import { People, Person } from './components/People'
import { Planets, Planet } from './components/Planets'
import { Starships, Starship } from './components/Starships'
import './App.css';
import Axios from 'axios';
// APP w/ CONTEXT PROVIDER
export default function App() {
return (
<>
<StarwarsProvider>
<AppContent />
</StarwarsProvider>
<ReactQueryDevtools initialIsOpen={false} />
</>
)
}
// CREATE CONTEXT
export const StarwarsContext = createContext()
// CONTEXT PROVIDER
function StarwarsProvider({ children }) {
// import categories
const categories = config.categories
// destructure default category of search selection
const { people } = categories
// initialize state
const [category, setCategory] = useState(people);
const [theID, setTheID] = useState('');
return (
<StarwarsContext.Provider value={{
category,
setCategory,
theID,
setTheID,
categories,
fetchStarwarsData
}}>
<AppContent />
</StarwarsContext.Provider>
)
}
async function fetchStarwarsData(category, id) {
if (!id) {
return
}
const response = await Axios.get(
`https://swapi.dev/api/${category}/${id}`
).then(res => res.data)
// const data = await response.json()
const data = response
// console.log(data)
return data
}
// APP CONTENT
const AppContent = memo(() => {
// import global state into component
const { category, setCategory } = useContext(StarwarsContext)
const { theID, setTheID } = useContext(StarwarsContext)
// destructure categories
const { categories: { people, planets, starships } } = useContext(StarwarsContext)
// log updates to ID and category
useWhatChanged([category, theID], 'category, theID')
// change state on input from user
const addId = e => setTheID(e.target.value)
const selectCategory = (e) => setCategory(e.target.value)
// route the user based on category
useEffect(() => navigate(`/${category}`), [category])
// search swapi based on category and id
const searchSwapi = e => {
e.preventDefault()
navigate(`/${category}/${theID}`)
}
return (
<div className="App">
<header className='App-header' >
Search For:
<form onSubmit={searchSwapi}>
<select onChange={selectCategory} className='form-control-lg bg-dark text-white'>
<option value={people} >People</option>
<option value={planets} >Planets</option>
<option value={starships} >Starships</option>
</select>
ID:
<input type='text' onChange={addId} className='form-control-lg col-sm-1 bg-dark text-white' />
<button className='btn-lg btn-warning' >Search Item</button>
</form>
</header>
<Router>
<People path='/people/'>
<Person path=':personId' fetchStarwarsData />
</People>
<Planets path="/planets/">
<Planet path=':planetId' fetchStarwarsData />
</Planets>
<Starships path='/starships/'>
<Starship path=':starshipId' fetchStarwarsData />
</Starships>
</Router>
</div>
)
})
const config = {
categories: {
people: 'people',
planets: 'planets',
starships: 'starships'
}
}
People.js
// People.js
import React from 'react'
import { useQuery } from "react-query";
import { StarwarsContext, StarwarsProvider } from "../App"
export const People = props => {
return (
<div>
<span> the People have spoken</span>
{props.children}
</div>
)
}
export const Person = () => {
const { category, theID, fetchStarwarsData } = React.useContext(StarwarsContext)
const { data, isLoading, error } = useQuery([category, theID], fetchStarwarsData)
if (isLoading) return <div>loading...</div>
if (error) return <div>oop!! error ocurred</div>
return (
<div>
<h1>/{category}/{theID}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
Planets.js
// Planets.js
import React from 'react'
import { useQuery } from "react-query";
import { StarwarsContext, StarwarsProvider } from "../App"
export const Planets = props => {
return (
<div>
<span> the Planets have spoken</span>
{props.children}
</div>
)
}
export const Planet = props => {
const { category, theID, fetchStarwarsData } = React.useContext(StarwarsContext)
const { data, isLoading, error } = useQuery([category, theID], fetchStarwarsData)
if (isLoading) return <div>loading...</div>
if (error) return <div>oop!! error ocurred</div>
return (
<div>
<h1>/{category}/{theID}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
Starships.js
// Starships.js
import React from 'react'
import { useQuery } from "react-query";
import { StarwarsContext, StarwarsProvider } from "../App"
export const Starships = props => {
return (
<div>
<span> the Starships have spoken</span>
{props.children}
</div>
)
}
export const Starship = () => {
const { category, theID, fetchStarwarsData } = React.useContext(StarwarsContext)
const { data, isLoading, error } = useQuery([category, theID], fetchStarwarsData)
if (isLoading) return <div>loading...</div>
if (error) return <div>oop!! error ocurred</div>
return (
<div>
<h1>/{category}/{theID}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
In my code I have a functional component that renders a list of the users in the database using a "load more" pagination, and a component that shows the details of a user alongside some actions when you click on them is the list, similarly to an file explorer. One of those actions is to delete the user. When you do so, the list of users doesn't update, and if you click the user you just deleted, an execution error will appear, for the code tries to use information from a null object. What is the best way to update the list component upon user deletion without using class components?
App.js - the parent of the list and detail components
<BrowserRouter>
<div id = "app">
<div className = "section sectionTop">
<Route path = "/" component = {BaseTop}/>
</div>
<div className = "Main">
<div className = "section sectionMain sectionLeft">
<Route path = "/" component = {BaseLeft}/>
</div>
<div className = "section sectionMain sectionCenter">
<Route path = "/" exact component = {BaseCenter}/>
<Route path = "/listusers" component = {UserList}/>
</div>
<div className = "section sectionMain sectionRight">
<Route path = "/" exact component = {BaseRight}/>
<Route path = "/adduser" component = {UserAdd}/>
<Route path = "/listusers/:id" component = {UserInfo}/>
</div>
</div>
</div>
</BrowserRouter>
UserList.js - the list of users
const [users, setUsers] = useState ([]);
const [page, setPage] = useState (0);
useEffect
(
() =>
{
setPage (page+1);
},
[]
);
useEffect
(
() =>
{
try
{
const runEffect = async () =>
{
const response = await api.get
(
`/users?page=${page}`
);
setUsers ([...users, ...response.data.docs]);
}
runEffect();
}
catch (error)
{
console.log (error);
}
},
[page]
);
return (
<div className = "userListArea">
{
users.map
(
(user, index) =>
{
return (
<div key = {index} className = "user">
<Link key = {index} to = {`/listusers/${user._id}`}>
<button
className = "buttonUser"
key = {index}>
{user.name}
</button>
</Link>
</div>
)
}
)
}
<button
className = "buttonLoadMore"
onClick = {() => setPage (page+1)}>
Carregar mais
</button>
</div>
)
UserInfo.js - the details of an user
const [user, setUser] = useState ({});
const [_id, set_id] = useState ("");
useEffect
(
() =>
{
if (user.hasOwnProperty ("_id") === false)
{
set_id (match.params.id);
}
else
{
if (user._id !== match.params.id)
{
set_id (match.params.id);
}
}
const runEffect = async () =>
{
const response = await api.get
(
"/searchuser",
{
params:
{
_id
}
}
)
if (user.hasOwnProperty ("name") === false)
{
setUser (response.data);
}
else
{
if (user.name !== response.data.name)
{
setUser (response.data);
}
}
}
runEffect();
}
);
async function handleDeleteUser (_id)
{
if (window.confirm(`Você realmente deseja remover o usuário ${user.name}?`))
{
const response = await api.delete
(
"/users",
{
params:
{
_id
}
}
);
if (response.data._id === _id)
{
window.alert(`O usuário ${user.name} foi excluído.`);
}
}
}
return (
<div className = "userInfoArea">
<div className = "name">{user.name}</div>
<div className = "email">{user.email}</div>
<button className = "buttonEdit">Editar</button>
<Link to = "/listusers">
<button
className = "buttonDelete"
onClick = {() => handleDeleteUser (user._id)}
>
Excluir
</button>
</Link>
</div>
)
You cannot update the state of a component that is not a parent or a child without using an external state management solution, such as Redux or the new useContext api/hook. These libraries involve moving all individual component's states to a larger centralized global state, which can be updated and accessed by all components.
First of all, good evening. I'm trying to improve myself at React. So I'm working on a Starwars project 👨💻.
I have two problems.
First of all, I listed different characters at the bottom of my character detail page. Again, I want it to be directed to different characters through the same component. But even if the link changes, my component is not refreshed. But the picture is changing 🤔.
Note:
sandbox link : https://codesandbox.io/s/github/kasim444/Javascript-Camp-2019/tree/master/challenges/star-wars-app/
my project github link : https://github.com/kasim444/Javascript-Camp-2019/tree/master/challenges/star-wars-app/
// component that I redirect in.
class CharacterDetail extends Component {
render () {
const characterId = this.props.match.params.id;
const {
name,
height,
mass,
hair_color,
skin_color,
eye_color,
birthday_year,
gender,
homeworld,
loading,
} = this.state;
return loading
? <Loading />
: (
<div>
<main className="characterBg">
<DetailHeader imgLink={characterAvatarLink[characterId - 1]} />
<CharacterContent
imgLink={characterAvatarLink[characterId- 1]}
characterInfo={this.state}
/>
</main>
<FeaturedCharacters />
</div>
);
}
}
// feautered character component
function FeaturedCharacters () {
const [characters, setCharacters] = useState ([]);
const [loading, setLoading] = useState (true);
const fetchCharacters = async () => {
const data = await fetch ('https://swapi.co/api/people/');
const fetchPeople = await data.json ();
const feauteredCharacter = fetchPeople.results.filter (
(character, index) => index < 4
);
setCharacters (feauteredCharacter);
setLoading (false);
};
useEffect (() => {
fetchCharacters ();
}, []);
return (
<div>
<h2>Popular Characters</h2>
<div className="d-flex-row container">
{loading
? <PlaceholderDiv />
: characters.map ((character, index) => (
<CharacterCard
key={character.name}
chaId={index + 1}
chaDet={character.name}
imgLink={characterAvatarLink[index]}
/>
))}
</div>
</div>
);
}
// character card link component
const CharacterCard = props => {
const name = props.chaDet;
return (
<Link className="profile_card" to={`/character/${props.chaId}`}>
<div className="profile_image">
<img src={props.imgLink} alt={name} />
</div>
<div className="profile_content">
<h3>{name}</h3>
<div className="read_more d-flex-row">
<img src={showIcon} alt="Show Icon" />
Show More
</div>
</div>
</Link>
);
};
// main component
const App = () => {
return (
<Router>
<div className="st-container d-flex-column">
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/movie/:title" component={MovieDetails} />
<Route path="/character/:id" component={CharacterDetail} />
<Route
path="/githubProfile"
component={() => {
window.location.href = 'https://github.com/kasim444/Javascript-Camp-2019/tree/master/challenges/star-wars-app';
return null;
}}
/>
</Switch>
<Footer />
</div>
</Router>
);
};
My second problem is that I draw a data back from the data from Api. I can reach outlines of the character. It's working now. But I feel that there are some things that don't feel right.🤨 How can I improve Fetch operations in Axios?
async componentDidMount () {
const characterId = this.props.match.params.id;
const filmSeries = [];
const characterDetail = await axios.get (
`https://swapi.co/api/people/${characterId}/`
);
const filmsFetchLinks = characterDetail.data.films;
const promisesData = await filmsFetchLinks.map(link => axios.get(link));
axios.all (promisesData).then(value => {
value.map (val => filmSeries.push (val.data.title));
let {
name,
height,
mass,
hair_color,
skin_color,
eye_color,
birthday_year,
gender,
homeworld,
films,
} = characterDetail.data;
fetch(homeworld).then(home => home.json()).then(val => this.setState({homeworld: val.name}));
this.setState ({
name,
height,
mass,
hair_color,
skin_color,
eye_color,
birthday_year,
gender,
films: filmSeries,
loading: false,
});
});
}
I'm sorry if I bored you. It seems a little long because the components are interconnected. Thank you in advance for your interest. 🖖 🙏
You can use componentDidUpdate and compare the the current parameter id to the previous one (you would have to save it to state) and you fetch data again IFF the two are different. componentDidUpdate will go every time the route changes.
A better approach would be to use useEffect and depend on the parameter id. In the use effect, you do all your data fetching. Something like this:
const { id: characterId } = props.match.params;
React.useEffect(() => {
async function getData() {
// fetch all you data here
}
getData();
}, [characterId]);
You can see a crude version of this here:
https://codesandbox.io/s/star-wars-app-sx8hk