I need to render a component that has a route using react router. the first component has a button that when clicked needs to render another component that has state passed in from the first component. All objects and strings from the first component show in the console.log of the child component but it wont set state when I use setProfile(p).
const Member = (props)=> {
const [user, setUser] = useState({});
const [profile, setProfile] = useState({});
// run effect when user state updates
useEffect(() => {
const doEffects = async () => {
try {
const pro = socialNetworkContract.members[0]
console.log(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
const a = await snInstance.getUsersPosts(pro, { from: accounts[0] });
console.log(a)
console.log(p)
setProfile(p)
} catch (e) {
console.error(e)
}
}
doEffects();
}, [profile, state]);
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div class="container">
<a target="_blank">Name : {profile.name}</a>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
{p.message}
</tr>})}
</div>
)
}
export default Member;
This is the parent component I want to redirect from
const getProfile = async (member) => {
const addr = dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default withRouter(Posts);
I have this component working when I don't have a dynamic route that needs data passing in from the parent component It's redirecting from.
This is my routes.js file
const Routes = () => {
return (
<Switch>
<Route path="/posts" exact component={Posts} />
<Route path="/member" exact component={Member} />
<Redirect exact to="/" />
</Switch>
)
}
export default Routes
https://codesandbox.io/s/loving-pine-tuxxb
Related
I am trying to implement Protected Routes in my app. I am using cookie-based session authentication.
The issue is: Whenever I try to access a protected page for the first time, the RequireAuth component has the isAuthenticated value as false and hence it navigates to /.
From the console logs, I can see Inside require auth. before Inside provide auth..
Questions:
Is using useEffect in the context provider the right way to set the auth state?
How do I make sure that the context provider state is set before accessing the context in the consumer RequireAuth?
I have a context provider ProvideAuth which makes an API call to check if the user is already authenticated.
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
Auth context consumer
export const useAuth = () => {
return useContext(authContext);
};
I use the context in a RequireAuth component to check if the user is already authenticated and redirect if not.
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo } = useAuth();
const location = useLocation();
useEffect(() => {
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, []);
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
The context provider is used in the App.js
return (
<ProvideAuth>
<div className='App'>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
<Route element={<RequireAuth /> }>
<Route path="/jobs" element={<Jobs />} >
<Route index element={<MyJobs />} />
<Route path="new" element={<NewJob />} />
<Route path=":jobId" element={<JobDetails />} />
<Route path=":jobId/stats" element={<JobStats />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</ProvideAuth>
);
That's because that useEffect in ProvideAuth is as any useEffect an asynchronous task, which means the component and its children may render before its callback gets executed.
A solution is to set up a loading state in ProvideAuth, called for example isCheckingAuth, set to true by default, and to false after you have done all the fetching. And you pass it down to RequireAuth, like so :
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
setIsCheckingAuth(false)
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo,
isCheckingAuth
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
You use that isCheckingAuth inRequireAuth to show a loader while the fetching is being done, this way:
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo, isCheckingAuth } = useAuth();
const location = useLocation();
useEffect(() => {
if(isCheckingAuth) return;
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, [isCheckingAuth]);
if(isCheckingAuth) return <div>Loading...</div>
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
What you can do is check, If the request is processed or not. If processing show loader if any error shows some error msg or redirect. If everything is fine load provider.
const authContext = createContext();
export function ProvideAuth({ children }) {
const [state, setState] = useState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
const isAuthenticated = await CheckAuthentication();
if (isAuthenticated) {
const user = await GetUserInfo();
setState((prev) => ({ ...prev, isAuthenticated, user }));
}
} catch (error) {
setState((prev) => ({ ...prev, error }));
} finally {
setState((prev) => ({ ...prev, isLoading: false }));
}
};
fetchData();
}, []);
if (state.isLoading) return <Loading />;
if (state.error) return <ErrorMessage error={state.error} />;
return <authContext.Provider value={state}>{children}</authContext.Provider>;
}
I'm very new to React - so bear with me.
I'm trying to create a set of authentication protected routes/components. I have the below code that I am using to achieve that.
However, my issue is that when the child component loads, the userInfo is {} (i.e. not set). I know that the userInfo is being returned from the userService as my console.log returns the correct data.
Am I going about this right? I want to be able to protect a component/route, and pass through the userInfo to any protected route so I can do stuff with the data in the respective component.
const UserAuthenticatedRoute = ({component: Component, ...rest}) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userInfo, setUserInfo] = useState({});
useEffect(async () => {
const r = await userService.isUserLoggedIn();
console.log(r.status);
if (r.status === 200){
setIsLoggedIn(true);
const userInfo = await r.json();
console.log(userInfo);
setUserInfo(userInfo);
} else {
setIsLoggedIn(false);
}
}, []);
return (
<Route {...rest} render={props => (
<>
<main>
{isLoggedIn &&
<Component {...props} userInfo={userInfo}/>
}
</main>
</>
)}
/>
);
};
I need to show the props value (which is a simple string). Each time I get new search results, I'm sending in the props. At the very first render the props will always be undefined.
Edit:
Header.jsx
function Header() {
const [searchString, setString] = useState('');
const onChangHandler = (e) => {
setString(e.target.value);
};
const activeSearch = () => {
if (searchString.length > 0) {
<Home searchResults={searchString} />;
}
};
return (
<div>
<input
placeholder='Search here'
value={searchString}
onChange={(e) => onChangHandler(e)}
/>
<button onClick={activeSearch}>Search</button>
</header>
</div>
);
}
I searched for previous stackoverflow questions and reactjs.org but found no answer.
Home.jsx
import React, { useEffect, useState } from 'react';
function Home({ searchResults }) {
const [itemSearchResults, setResults] = useState([]);
const [previousValue, setPreviousValue] = useState();
// What function will re-render when the props are first defined or changed ?
useEffect(() => { // Doesn't work
setResults(searchResults);
}, [searchResults]);
return (
<div>
<h3>Home</h3>
<h1>{itemSearchResults}</h1>
</div>
);
}
export default Home;
App.js
function App() {
return (
<div className='App'>
<Header />
<Home />
<Footer />
</div>
);
}
I'm sending the input string only to check if the props will change at the child component ("Home").
Any experts here know what's the problem?
Why it doesn't work?
It's because the Home component is never used, even if it's included in the following snippet:
const activeSearch = () => {
if (searchString.length > 0) {
<Home searchResults={searchString} />;
}
};
The activeSearch function has a couple problems:
it is used as an event handler though it uses JSX (outside the render phase)
it doesn't return the JSX (would still fail inside the render phase)
JSX should only be used within the render phase of React's lifecycle. Any event handler exists outside this phase, so any JSX it might use won't end up in the final tree.
The data dictates what to render
That said, the solution is to use the state in order to know what to render during the render phase.
function Header() {
const [searchString, setString] = useState('');
const [showResults, setShowResults] = useState(false);
const onChangHandler = (e) => {
// to avoid fetching results for every character change.
setShowResults(false);
setString(e.target.value);
};
const activeSearch = () => setShowResults(searchString.length > 0);
return (
<div>
<input
value={searchString}
onChange={(e) => onChangHandler(e)}
/>
<button onClick={activeSearch}>Search</button>
{showResults && <Home query={searchString} />}
</div>
);
}
useEffect to trigger effects based on changing props
And then, the Home component can trigger a new search request to some service through useEffect.
function Home({ query }) {
const [results, setResults] = useState(null);
useEffect(() => {
let discardResult = false;
fetchResults(query).then((response) => !discardResult && setResults(response));
// This returned function will run before the query changes and on unmount.
return () => {
// Prevents a race-condition where the results from a previous slow
// request could override the loading state or the latest results from
// a faster request.
discardResult = true;
// Reset the results state whenever the query changes.
setResults(null);
}
}, [query]);
return results ? (
<ul>{results.map((result) => <li>{result}</li>))}</ul>
) : `Loading...`;
}
It's true that it's not optimal to sync some state with props through useEffect like the article highlights:
useEffect(() => {
setInternalState(externalState);
}, [externalState]);
...but in our case, we're not syncing state, we're literally triggering an effect (fetching results), the very reason why useEffect even exists.
const { useState, useEffect } = React;
const FAKE_DELAY = 5; // seconds
function Home({ query }) {
const [results, setResults] = useState(null);
useEffect(() => {
let queryChanged = false;
console.log('Fetch search results for', query);
setTimeout(() => {
if (queryChanged) {
console.log('Query changed since last fetch, results discarded for', query);
return;
}
setResults(['example', 'result', 'for', query])
}, FAKE_DELAY * 1000);
return () => {
// Prevent race-condition
queryChanged = true;
setResults(null);
};
}, [query]);
return (
<div>
{results ? (
<ul>
{results.map((result) => (
<li>{result}</li>
))}
</ul>
) : `Loading... (${FAKE_DELAY} seconds)`}
</div>
);
}
function Header() {
const [searchString, setString] = useState('');
const [showResults, setShowResults] = useState(false);
const onChangHandler = (e) => {
// to avoid fetching results for every character change.
setShowResults(false);
setString(e.target.value);
};
const activeSearch = () => setShowResults(searchString.length > 0);
return (
<div>
<input
placeholder='Search here'
value={searchString}
onChange={(e) => onChangHandler(e)}
/>
<button onClick={activeSearch}>Search</button>
{showResults && <Home query={searchString} />}
</div>
);
}
ReactDOM.render(<Header />, document.querySelector("#app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Better solution: Uncontrolled inputs
Another technique in your case would be to use an uncontrolled <input> by using a ref and only updating the search string on click of the button instead of on change of the input value.
function Header() {
const [searchString, setString] = useState('');
const inputRef = useRef();
const activeSearch = () => {
setString(inputRef.current.value);
}
return (
<div>
<input ref={inputRef} />
<button onClick={activeSearch}>Search</button>
{searchString.length > 0 && <Home query={searchString} />}
</div>
);
}
const { useState, useEffect, useRef } = React;
const FAKE_DELAY = 5; // seconds
function Home({ query }) {
const [results, setResults] = useState(null);
useEffect(() => {
let queryChanged = false;
console.log('Fetch search results for', query);
setTimeout(() => {
if (queryChanged) {
console.log('Query changed since last fetch, results discarded for', query);
return;
}
setResults(['example', 'result', 'for', query])
}, FAKE_DELAY * 1000);
return () => {
// Prevent race-condition
queryChanged = true;
setResults(null);
};
}, [query]);
return (
<div>
{results ? (
<ul>
{results.map((result) => (
<li>{result}</li>
))}
</ul>
) : `Loading... (${FAKE_DELAY} seconds)`}
</div>
);
}
function Header() {
const [searchString, setString] = useState('');
const inputRef = useRef();
const activeSearch = () => {
setString(inputRef.current.value);
}
return (
<div>
<input
placeholder='Search here'
ref={inputRef}
/>
<button onClick={activeSearch}>Search</button>
{searchString.length > 0 && <Home query={searchString} />}
</div>
);
}
ReactDOM.render(<Header />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Passing the state around
[The following line] brings the Home component inside the Header component, which makes duplicate
{searchString.length > 0 && <Home query={searchString} />}
In order to make the Header component reusable, the quickest way would be to lift the state up.
// No state needed in this component, we now receive
// a callback function instead.
function Header({ onSubmit }) {
const inputRef = useRef();
const activeSearch = () => {
// Uses the callback function instead of a state setter.
onSubmit(inputRef.current.value);
}
return (
<div>
<input ref={inputRef} />
<button onClick={activeSearch}>Search</button>
</div>
);
}
function App() {
// State lifted up to the parent (App) component.
const [searchString, setString] = useState('');
return (
<div className='App'>
<Header onSubmit={setString} />
{searchString.length > 0 && <Home query={searchString} />}
<Footer />
</div>
);
}
If that solution is still too limited, there are other ways to pass data around which would be off-topic to bring them all up in this answer, so I'll link some more information instead:
Thinking in React
What's the right way to pass form element state to sibling/parent elements?
Passing data to sibling components with react hooks?
Application State Management with React
How can I update the parent's state in React?
Top 5 React state management libraries in late 2020 (Redux, Mobx, Recoil, Akita, Hookstate)
if your props are passed as searchResults, then change the props to,
function Home({ searchResults}) {...}
and use
useEffect(() => { // code, function },[searchResults]) ).
I have a react component I need to render that takes one argument of a string when it is initialized. I need a button to click on that will redirect and render this new component with the string. It sends the string I want when it console.log(pro). everytinme I click on the button it goes to a blank screen and doesn't load.
My routes.js looks like
const Routes = (props) => {
return (
<Switch>
<Route exact path="/member" component={() => <Member user={props.state.member} />} />
<Route path="/posts" exact component={Posts} />
<Redirect exact to="/" />
</Switch>
)
}
export default Routes
The original component looks like this
const Posts = (props) => {
const dispatch = useDispatch();
const [postCount, setPostCount] = useState(0);
const [member, setMember] = useState({});
const getProfile = async (member) => {
const addr = dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
props.history.push('/member'
);
console.log('----------- member------------') // console.log(addr)
return (
<Member user={member}><Member/>
);
}
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default withRouter(Posts);
The component I'm trying to render from the Posts component needs to be rendered with a string
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = props.location;
const [profile, setProfile] = useState({});
useEffect(() => {
const doEffects = async () => {
try {
const pro = socialNetworkContract.members[0]
console.log(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
setProfile(p)
} catch (e) {
console.error(e)
}
}
doEffects();
}, [profile]);
return (
<div class="container">
{profile.name}
</div>
)
}
export default Member;
You can pass an extra data to a route using state attribute with history.push
const getProfile = async (member) => {
const addr = dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
props.history.push({
path: '/member',
state: { member }
});
}
Once you do that you can access it in the rendered route from location.state
import {
useLocation
} from "react-router-dom";
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = useLocation();
console.log(state.member);
const [profile, setProfile] = useState({});
...
}
export default Member;
Also you do not need to pass on anything while rendering the Route
const Routes = (props) => {
return (
<Switch>
<Route exact path="/member" component={Member} />
<Route path="/posts" exact component={Posts} />
<Redirect exact to="/" />
</Switch>
)
}
export default Routes
I'm trying to redirect page declaratively.
Here is my Header.js:
const Header = () => {
const [data, setData] = useState({ objects: [] });
useEffect(async () => {
const result = await axios.get(
"http://example.com/api/v1/categories"
);
setData(result.data);
}, []);
return (
<div>
{courses.map( objects => (
<Link to={`/cats/${objects.id}`}>{objects.title}</Link>
))}
</div>
);
};
export default Header;
Here is my App.js
class App extends Component {
render(){
return (
<Router>
<Redirect exact from="/" to="/index" />
<Route path = "/" component = {App}>
<Header/>
<Route path="/cats/:objectId" component={CoursePage} />
</Route>
</Router>
);
}
}
export default App;
Here is the CoursePage.js:
const CoursesPage = (props) => {
let { objectId } = useParams();
const [data, setData] = useState({ courses: [] });
useEffect(() => {
(async () => {
const result = await axios.get(
"http://example.com/api/v1/cats/"+objectId
).catch(err => {
console.error(err);
});
setData(result.data);
})();
}, []);
return (
<Fragment>
{courses.title}
</Fragment>
);
};
export default CoursesPage;
On Header I click to links. It redirects to course page successfully. But when I click again another course link, it doesn't reload the component and it doesn't load the new data. The URL changes, but page is not.
How can I force the page to reload the component?
You should change the dependency array of useEffect in CoursePage to depend on objectId, whenever object id will change, it will rerun the effect.
const CoursesPage = (props) => {
let { objectId } = useParams();
const [data, setData] = useState({ courses: [] });
useEffect(() => {
(async () => {
setData({ courses: [] }); // add this line
const result = await axios.get(
"http://example.com/api/v1/cats/"+objectId
).catch(err => {
console.error(err);
});
setData(result.data);
})();
}, [objectId]); // Update dependency array here
return (
<Fragment>
{courses.title}
</Fragment>
);
}
export default CoursesPage;
This will reset the previous data before to load new one as soon as object id changes.
If you want to remount component completely on param change in url, then you can add an id to your component root element.
return (
<Fragment key={objectId}>
{courses.title}
</Fragment>
);
More Info here...