I am creating an example dApp which carries the "Header" component at the top of the page for every page. So, I have created a header component and I make people connect to their MetaMask wallet in Header.tsx, which they do successfully and I keep their wallet ID with currentAccount state.
Header.tsx:
const Header: FunctionComponent<{}> = (props) => {
const [currentAccount, setCurrentAccount] = useState("");
async function checkAccount() {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
setCurrentAccount(accounts[0]);
}
return (
<header>
<div className="col-xl-3 col-lg-3 col-md-3 col-sm-3">
<ul>
<li>{!connectHidden && <button className="buy connect-wallet" onClick={connectWallet}><b>Connect Wallet</b></button>}</li>
</ul>{currentAccount}
<ul>
<li>{!disconnectHidden && <button className="buy connect-wallet" onClick={disconnectWallet}><b>Disconnect Wallet</b></button>}</li>
</ul>
</div>
</header>
);
};
export default Header;
But at my homepage, there are no codes for anything about getting user's wallet ID, I don't want to rewrite the code here as it is not the right way. As a newbie in react, I couldn't make the codes I have tried work like trying to import a function or variables. How do I call the currentAccount state in my home page?
Home.tsx:
const HomePage: FunctionComponent<{}> = () => {
useEffect(() => {
onInit()
return () => { }
}, [])
async function onInit() {
}
async function onClickMint() {
alert("mint");
}
return (
<>
<div>xx
</div>
</>
);
};
export default HomePage;
Here is my app.tsx and as you can see, I am seeing all of the components at once. But I want to use the state I have got at Header component in my Home component.
App.tsx:
import Header from './components/Header';
const App: FunctionComponent<{}> = () => {
return (
<Router>
<Header />
<Route exact path="/" component={HomePage} />
<Route exact path="/wallet" component={Wallet} />
<Footer />
</Router>
);
};
export default App;
Quick answer:
simply create your state at the top level (App.tsx) and give currentAccount, setCurrentAccount as props for the other components
App.tsx:
import Header from './components/Header';
const App: FunctionComponent<{}> = () => {
const [currentAccount, setCurrentAccount] = useState("");
return (
<Router>
<Header />
<Route exact path="/">
<HomePage currentAccount={currentAccount} setCurrentAccount={setCurrentAccount}/>
</Route>
<Route exact path="/wallet">
<Wallet currentAccount={currentAccount} setCurrentAccount={setCurrentAccount}/>
</Route>
<Footer />
</Router>
);
};
export default App;
Longer answer:
You need to inform yourself about redux or simply the useContext hook
For instance with the useContext hook you can create a context that will contain your state and that you will be able to access in any child component without using props which can be redundant when you have multiple children and grandchildren ...
Here you can find the documentation about how to use the useContext Hook :
https://reactjs.org/docs/hooks-reference.html#usecontext
Related
Yo-yo everyone,
along my path of practicing the art of React, I noticed a bug that I couldn't seem to find a good source to help me understand what causes the problem.
My array in a child component takes too long to load, resulting in an error.
The data is fetched from "jsonplaceholder," users list.
Data is set as a state.
Sent to "UserProfilePage".
Sent to "UserProfileComponent".
Trying to reach the URL "/user/1" will not succeed since the object is undefined.
*) Commenting the "UserProfileComponent," and then uncomment without refreshing will successfully load the page.
*) Coping (not fetching) the data to the App.js, assigning it to the state, will not crush the system.
APP.js
import { Component } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import "./App.css";
import Navigation from "./components/header/Navigation";
import PostsLog from "./components/Posts/PostsLog";
import UserProfileCollection from "./pages/UserProfileCollection";
import UserProfilePage from "./pages/UserProfilePage";
const POST_ENDPOINT = "https://jsonplaceholder.typicode.com/posts";
const USER_ENDPOINT = "https://jsonplaceholder.typicode.com/users";
class App extends Component {
constructor() {
super();
this.state = {
exUsersArray: [],
exPostsArray: [],
};
}
async componentDidMount() {
try {
const responseUser = await fetch(USER_ENDPOINT);
const responsePost = await fetch(POST_ENDPOINT);
const dataResponseUser = await responseUser.json();
const dataResponsePost = await responsePost.json();
this.setState({ exUsersArray: dataResponseUser });
this.setState({ exPostsArray: dataResponsePost });
} catch (error) {
console.log(error);
}
}
render() {
const { exUsersArray, exPostsArray } = this.state;
console.log(exUsersArray);
return (
<div className="app">
<Navigation />
<main>
<Switch>
{/* REROUTES */}
<Route path="/" exact>
<Redirect to="/feed" />
</Route>
<Route path="/users" exact>
<Redirect to="/user" />
</Route>
{/* REAL ROUTES */}
<Route path="/feed">
<PostsLog usersInfo={exUsersArray} usersPosts={exPostsArray} />
</Route>
<Route path="/user" exact>
<UserProfileCollection usersInfo={exUsersArray} />
</Route>
{/* DYNAMIC ROUTES */}
<Route path="/user/:userId">
<UserProfilePage usersInfo={exUsersArray} />
</Route>
</Switch>
</main>
</div>
);
}
}
export default App;
UserProfilePage.js
import { useParams } from "react-router-dom"
import UserProfileComponent from "../components/UserProfileComponent";
const UserProfilePage = ({usersInfo}) => {
const params = useParams();
const foundUser = usersInfo.find((user) => Number(user.id) === Number(params.userId))
console.log("found user ", foundUser);
// console.log(usersInfo);
console.log(params, " is params");
return(
<div>
<UserProfileComponent userProfile={foundUser}/>
<p>Yo YO</p>
</div>
)
}
export default UserProfilePage;
UserProfileComponent
const UserProfileComponent = ({userProfile}) => {
console.log(userProfile)
return (
<div className="text-group">
<div className="wrap-post">
<p>
<strong>Info</strong>
</p>
<img
src={`https://robohash.org/${userProfile.Id}.png`}
id="small-profile"
alt="user profile in circle"
/>
<p><u><strong>ID</strong></u> : {userProfile.id}</p>
<p>Name: {userProfile.name}</p>
<p>#{userProfile.username}</p>
<p>Email: {userProfile.email}</p>
<p>
{userProfile.address.street} {userProfile.address.suite}<br/>
{userProfile.address.zipcode} {userProfile.address.city}
</p>
<p>Global position</p>
<p>{userProfile.address.geo.lat}, {userProfile.address.geo.lang}</p>
<p>{userProfile.phone}</p>
<p>{userProfile.website}</p>
<p>Company</p>
<p>{userProfile.company.name}</p>
<p>{userProfile.company.catchPhrase}</p>
<p>{userProfile.company.bs}</p>
</div>
</div>
);
};
export default UserProfileComponent;
Complete repository here.
I will be happy to any tips to help me understand what happened here.
Appreciation will be given to any tip that will help me be a better programmer.
Best wishes y'all.
it seems like usersInfo hasn't loaded a quick way to fix it is to just add this to the users component.
UserProfilePage.js
import { useParams } from "react-router-dom"
import UserProfileComponent from "../components/UserProfileComponent";
const UserProfilePage = ({usersInfo}) => {
const params = useParams();
if(!usersInfo) {
return <p>Loading...</p>
}
const foundUser = usersInfo.find((user) => Number(user.id) === Number(params.userId))
console.log("found user ", foundUser);
// console.log(usersInfo);
console.log(params, " is params");
return(
<div>
<UserProfileComponent userProfile={foundUser}/>
<p>Yo YO</p>
</div>
)
}
export default UserProfilePage;
UserProfileComponent.js
const UserProfileComponent = ({userProfile}) => {
if(!userProfile) {
return <p>Loading...</p>
}
console.log(userProfile)
return (
<div className="text-group">
<div className="wrap-post">
<p>
I see that you're rendering your compoonent without doing any null check in UserProfileComponent. Actually to be a better programmer or doing better work, you have to control every null case in order not to crash your app.
<p><u><strong>ID</strong></u> : {userProfile.id}</p>
<p>Name: {userProfile.name}</p>
<p>#{userProfile.username}</p>
<p>Email: {userProfile.email}</p>
<p>
{userProfile.address.street} {userProfile.address.suite}<br/>
{userProfile.address.zipcode} {userProfile.address.city}
</p>
<p>Global position</p>
<p>{userProfile.address.geo.lat}, {userProfile.address.geo.lang}</p>
<p>{userProfile.phone}</p>
<p>{userProfile.website}</p>
<p>Company</p>
<p>{userProfile.company.name}</p>
<p>{userProfile.company.catchPhrase}</p>
<p>{userProfile.company.bs}</p>
You'll see that there's no null check. It would be better if you have some null check on your userProfile
Also, my suggestion is, you can create a loading in your state.
Before sending your request, you can set the loading to true.
And when your loading is true, you can show some spinner or sth like that. When your request finishes, you can set the loading variable to false and you can show your data.
The main point is, always use a loading variable to check the loading state instead of checking the null | undefined state of your data.
Instead of passing props from parent to child1(parent of child2) ->to child2,
I want to use createContext and receive the value with useContext.
What I tried to do is not correct because and I'm getting an error **'booleanContext' is not defined**.
How can I pass the createContext state/value ?
App.js
CreatePass is a component inside SignUp
const [signUpFirst, setIfSignUp] = useState(true);
const booleanContext = createContext(setIfSignUp);
return (
<booleanContext.Provider value={setIfSignUp}>
<div>
</Switch>
<Route exact path='/signup'>
<SignUp homePage={exitSignUpPage} setUserNumber={setUserID} />
</Route>
<Route exact path='/home'>
<Home userIDNumber={userID} setIfSignUp={setIfSignUp} />
</Route>
<CreatPass />
</Switch>
</div>
</booleanContext.Provider>
);
SignUp.js
render() {
return (
<div className='signUp-div'>
<Header />
<Router history={history}>
<div className='form-div'>
<Redirect to='/signup/mobile' />
<Switch>
<Route exact path='/signup/mobile' component={MobileNum} />
<Route exact path='/signup/idnumber'>
<IdentNumber setPersonalID={this.props.setUserNumber} />
</Route>
<Route exact path='/signup/password'>
<CreatePass />
</Route>
</Switch>
</div>
</Router>
</div>
);
}
CreatePass.js //'booleanContext' is not defined no-undef
const CreatePass = () => {
const changeBooleanValue = useContext(booleanContext);
const handleClickButton = () => {
changeBooleanValue(true);
};
return (
<div className='form-div'>
<button onClick={handleClickButton}>Click</button>
</div>
);
};
export default CreatePass;
Edit - Update:
This solution is not good it's the same value as I did above, booleanContext is undefined -
export const booleanContext = createContext(); // default value is undefiend
...
const App = () =>
return(
<booleanContext.Provider value={setIfSignUp}>
<CreatPass />
</booleanContext.Provider>
)
}
export default App;
Will be glad for explanations
for context provider and consumer, usually I create 1 hook for it like useBoolean.tsx that return BooleanProvider component that you can put inside your parent and useBoolean function that you can import inside your child component.
Basically any global function should wrap all its children inside its Provider.
So, if you want <CreatePass/> to access the value from the provider, it needs to be a children of its Provider
<BooleanProvider>
<CreatePass/>
</BooleanProvider>
useBoolean.tsx will have something like this
// useBoolean.tsx
const BooleanContext = createContext()
export const BooleanProvider = ({ children }) => {
const [signUpFirst, setIfSignUp] = useState(true)
return (
<BooleanContext.Provider value={{ signUpFirst, setIfSignUp }}>
{children}
</BooleanContext.Provider>
)
}
export const useBoolean = () => {
const context = useContext(BooleanContext)
if (context === undefined) {
throw new Error(`${useBoolean.name} must be within ${BooleanProvider.name}`)
}
return context
}
Parent component will be like usual
// Parent component
import { BooleanProvider } from 'hooks/useBoolean'
const Parent = () => (
<BooleanProvider>
<Child1 />
</BooleanProvider>
)
Child1 will have Child2 and Child2 will use the useBoolean function we created
// Child1 component
const Child1 = () => (
<div>
<Child2 />
</div>
)
// Child2 component
import { useBoolean } from 'hooks/useBoolean'
const Child2 = () => {
const { setIfSignUp } = useBoolean()
return <div></div>
}
Hope this helps clarify something for you. I am just trying to explain my understanding here on using the react context api and my way of doing it as well.
I tried to create hooks function like this for easier code management - it helps me understand what this context does directly with hooks naming convention. The hook function throws error too when it is not being used inside the Provider :D. Cheers 🙏!
Move "const booleanContext = createContext(setIfSignUp);" Outside of the component, you dont want to change context reference every render. Also export it for other components.
export const booleanContext = createContext(); // default value is undefiend
...
const App = () => ...
export default App;
Then in CreatePass, you should import context (used named export, so import would be like this:
import { booleanContext } from '..somePath../App'
Now it should work.
EDIT:
Works as expected
https://codesandbox.io/s/createcontexttest-0qvvm?file=/src/App.js
EDIT 2
<App>
<CreatePass /> // there is value undefined
<Context.Provider value={'something'}>
<CreatePass /> // value is defined
<SignUp>
<CreatePass /> // also defined
</SignUp>
</Context.Provider>
<div>
<CreatePass /> // undefined
</div>
</App>
Code looks ok. You can fix the default value error when exporting the context with an empty object {}.
export const booleanContext = createContext({});
export default function App() {
...
return (
<booleanContext.Provider value={setIfSignUp}>
...
</booleanContext.Provider>
)
}
and import it CreatePass.js
import React, { useContext } from "react";
import { booleanContext } from "./App";
const CreatePass = () => {
const changeBooleanValue = useContext(booleanContext);
...
}
first, create a custom component for your context provider:
import { createContext } from "react";
const CustomContext = createContext(["green", () => {}]);
export default CustomContext;
then wrap all your components using it in for example App.js:
<CustomContext.Provider value={options}>
[…]
</CustomContext.Provider>
and then you can use it as follows:
import CustomContext from "./CustomContext";
// top of SearchParams function body
const [options] = useContext(CustomContext);
Problem Context
I have a component named <Layout/> which contains my header and footer. It wraps all my routes as such:
<Layout>
<div className="container">
<Route path="/sign-in" exact component={SignIn}/>
<ProtectedRoute path="/" exact component={Home}/>
<ProtectedRoute path="/statistics" exact component={Statistics}/>
...
...
</div>
</Layout>
My <Layout/> component is defined as such:
const Layout = (props) => {
return (
<div>
<Header/>
{props.children}
<Footer/>
</div>
);
}
I did this so that i don't have to include my header and footer in each and every component.
Problem
In my header component, I am using the instance auth which indicates whether a user is logged in or not. The auth changes after the user signs in. However, even though the auth changes, my <Header/> component in the <Layout/> is not re-rendered. I have to manually refresh it to incorporate the changes.
My <Header/> component is defined as:
import auth from '../../auth';
const Header = () => {
return (
{auth.isAuth() ?
//render icon A
: null
}
<div>
Healthcare Management System //header title
</div>
{auth.isAuth() ?
//render icon B
: null
}
</div>
);
}
export default Header;
This is my auth class:
class Auth{
constructor(){
this.authenticated = JSON.parse(sessionStorage.getItem('profile'));
}
login(cb){
this.authenticated = JSON.parse(sessionStorage.getItem('profile'));
cb();
}
logout(cb){
this.authenticated = null;
cb();
}
isAuth(){
return this.authenticated;
}
}
export default new Auth();
What I require
What i want is supposedly simple; when the auth.isAuth() == null, show no icons and only the title (this behaves correctly). When, the auth.isAuth() != null, show icons A and B (this does not behave correctly and requires me to refresh the page in order to render the icons).
Somehow, i want the <Layout/> component to re-render once the auth changes. Thank you!
React component is only rerendered when its props or its state is changing, therefore you should put auth in a state which can be set as followed:
import auth from '../../auth';
const Layout = (props) => {
const [authenticated, setAuthenticated] = React.useState(false);
React.useEffect(() => {
if(!auth.isAuth()) {
setAuthenticated(false);
} else {
setAuthenticated(true);
}
}, []); // this useEffect logic is not working, you need to determine how you want to configure the condition in which authenticated state is set
return (
<div>
<Header authenticated={authenticated} />
{props.children}
<Footer/>
</div>
);
}
Header component:
const Header = ({ authenticated }) => {
return (
{authenticated ?
//render icon A
: null
}
<div>
Healthcare Management System //header title
</div>
{authenticated ?
//render icon B
: null
}
</div>
);
}
export default Header;
I'm using React Router for routing to different routes as below:
<Router>
<Switch>
<Route path="/teams/:teamName/matches" >
<MatchPage/>
</Route>
<Route path="/teams/:teamName" >
<TeamPage/>
</Route>
</Switch>
</Router>
Now in my TeamPage component I'm calling an API using async and then in the render method invoking another component called MatchDetailCard
class TeamPage extends Component {
constructor(props) {
console.log('const called')
super(props)
this.state = {
team: [],
teamName:null
}
}
async componentDidMount() {
console.log(this.props.match.params.teamName);
const teamName = this.props.match.params.teamName;
const response = await fetch(`http://localhost:8080/team/${teamName}`);
const json = await response.json();
console.log(json);
this.setState({team:json, teamName: teamName});
}
componentDidUpdate () {
console.log('updated')
}
render() {
if (!this.state.team || !this.state.team.teamName) {
return <h1>Team not found</h1>;
}
return (
<div className="TeamPage">
<div className="match-detail-section">
<h3>Latest Matches</h3>
<MatchDetailCard teamName={this.state.team.teamName} match={this.state.team.matches[0]}/>
</div>
</div>
);
}
}
export default withRouter(TeamPage);
Within the MatchDetailCard component I create a router Link to the same TeamPage component which will have a different teamName this time as below:
const MatchDetailCard = (props) => {
if (!props.match) return null;
const otherTeam = props.match.team1 === props.teamName ? props.match.team2 : props.match.team1;
const otherTeamRoute = `/teams/${otherTeam}`;
const isMatchWon = props.teamName === props.match.matchWinner;
return (
<div className={isMatchWon ? 'MatchDetailCard won-card' : 'MatchDetailCard lost-card'}>
<div className="">
<span className="vs">vs</span><h1><Link to={otherTeamRoute}>{otherTeam}</Link></h1>
</div>
</div>
);
}
export {MatchDetailCard};
The problem that I'm facing is that the on-click of the link to /team/teamName route only the TeamPage component is not mounting instead it's just getting an update.
I want to have the call to componentDidMount hook to make the API call again in this scenario.
What's the problem with my logic?
If the same component is used as the child of multiple <Route>s at the same point in the component tree, React will see this as the same component instance and the component’s state will be preserved between route changes. If this isn’t desired, a unique key prop added to each route component will cause React to recreate the component instance when the route changes.
https://reactrouter.com/web/api/Route
You can add the teamName as a key prop on the component, which will tell React to unmount/mount the component when the key value changes.
<Router>
<Switch>
<Route
path="/teams/:teamName/matches"
render={({ match }) => {
return <MatchPage key={match.params.teamName} />;
}}
/>
<Route
path="/teams/:teamName"
render={({ match }) => {
return <TeamPage key={match.params.teamName} />;
}}
/>
</Switch>
</Router>
The app displays all photos <Photo> in a grid <PhotoGrid>, then once clicked, a function in <Photo> changes URL with history.push, and Router renders <Single> based on URL using useParams hook.
PhotoGrid -> Photo (changes URL onClick) -> Single based on URL (useParams).
I must have messed something up, becouse useParams returns undefined.
Thanks for all ideas in advanced.
App.js
class App extends Component {
render() {
return (
<>
<Switch>
<Route exact path="/" component={PhotoGrid}/>
<Route path="/view/:postId" component={Single}/>
</Switch>
</>
)
}
}
export default App;
Photogrid.js
export default function PhotoGrid() {
const posts = useSelector(selectPosts);
return (
<div>
hi
{/* {console.log(posts)} */}
{posts.map((post, i) => <Photo key={i} i={i} post={post} />)}
</div>
)
}
in Photo I change URL with history.push
const selectPost = () => {
(...)
history.push(`/view/${post.code}`);
};
Single.js
import { useParams } from "react-router-dom";
export default function Single() {
let { id } = useParams();
console.log("id:", id) //returns undefined
return (
<div className="single-photo">
the id is: {id} //renders nothing
</div>
)
}
When using useParams, you have to match the destructure let { postId } = useParams(); to your path "/view/:postId".
Working Single.js
import { useParams } from "react-router-dom";
export default function Single() {
const { postId } = useParams();
console.log("this.context:", postId )
return (
<div className="single-photo">
{/* render something based on postId */}
</div>
)
}
You should use the same destructure as mentioned in your Route path. In this case, you should have written :
let { postID } = useParams();
I will mention two more mistakes which someone could make and face the same problem:
You might use Router component in place of Route component.
You might forget to mention the parameter in the path attribute of the Route component, while you would have mentioned it in the Link to component.
Ensure the component where you call useParams() is really a child from <Route>
Beware of ReactDOM.createPortal
const App = () => {
return (
<>
<Switch>
<Route exact path="/" component={PhotoGrid}/>
<Route path="/view/:postId" component={Single}/>
</Switch>
<ComponentCreateWithPortal /> // Impossible to call it there
</>
)
}
You have to check API that you are using. Sometimes it's called not just id. That's why useParams() do not see it