I have lots of static forms which i show the user when he clicks on the main menu and goes to a specific route, what i want to do now is to check when going to a route if that component has permission to be visited, i can do this by doing a simple post to server but i am confused and i don't know where should be the place to do this check or post.
Here are some of the solutions i thought of:
1- Writing a Higher order component and wrapping each static component with it
2- creating a base class and making each static form to inherit it while doing this check in the parent class
3- Or maybe using the routes as a solution since i am using the react-router ?
I will appreciate any help or tips.
Thanks.
Create a custom hook like so:-
const useAdmin = (url:string) => {
const [admin, setAdmin] = React.useState(false);
React.useEffect(() => {
post(url, {some: body}).then(res => {
setAdmin(res.admin);
}).catch(err => {
setAdmin(false);
});
}, [])
return [admin];
}
Then use it anywhere:-
const mycomponent = props => {
const [admin] = useAdmin('https://foobar.com/checkPermission');
//Rest of the logic according to the variable 'admin'
return (
<div>
{
admin? <div/>:null
}
</div>
)
}
Or think of admin as permission. Pass it some url for different routes and it will handle it.
I do something similar using react-router as well. I made my own route component that wraps around react-router's route that checks permissions and conditionally renders the route or redirects.
If you're doing the api call each time, then it would look something close to this.
class AppRoute extends Component {
state = {
validCredentials: null
}
componentDidMount() {
// api call here + response
if (ok) {
this.setState({validCredentials: true})
} else {
this.setState({ validCredentials: false})
}
}
render() {
const { validCredentials } = this.state
if (validCredentials) {
return <Route {...this.props} />
} else if (validCredentials === false) {
return <Redirect to="somewhere"/>
}
return null
}
}
You can definitely accomplish this using a Higher Order Component. Just set a state for the user on login like "admin" or "basic_user." According to this state some buttons or forms are going to be available for the user to access. You can also save these access permissions to your backend and call it in the HOC whenever the user logs in.
Related
I got a problem with my dynamic route. It look like this
[lang]/abc
I am trying to get query value from [lang] but when I using useRouter/withRouter i got query during 2-3 render of page ( on first i got query.lang = undefined ). its possible to get in 1 render or use any technique ?
I found something:
isReady: boolean - Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
https://nextjs.org/docs/api-reference/next/router#router-object
And the code would be like:
const router = useRouter();
useEffect(()=>{
if(!router.isReady) return;
// codes using router.query
}, [router.isReady]);
It's impossible to get the query value during the initial render.
Statically optimized pages are hydrated without the route parameters, so the query is an empty object ({}).
Next.js will populate the query after the page has been hydrated.
Next.js 10.0.5 and up
To determine if the route params are ready, you can use router.isReady inside a useEffect hook. For an example, see the answer provided by #doxylee.
Before Next.js 10.0.5
At first render of a dynamic route router.asPath and router.route are equal. Once query object is available, router.asPath reflects it.
You can rely on the query value within a useEffect hook after asPath has been changed.
const router = useRouter();
useEffect(() => {
if (router.asPath !== router.route) {
// router.query.lang is defined
}
}, [router])
GitHub Issue - Add a "ready" to Router returned by "useRouter"
In NextJS 9+, one way to ensure route parameters are immediately available for page components is to get them from the context arg passed to getServerSideProps() and pass to the component as props.
For a page like [id].js,
export function getServerSideProps(context) {
return {
props: {params: context.params}
};
}
export default ({params}) => {
const {id} = params;
return <div>You opened page with {id}</div>;
};
This is a great question and one that took a few days for me to figure out what the best approach is.
I have personally found three viable solutions to the problem of validating dynamic route path params or even just route path params in general.
The three solutions are
SSR (don't recommend) [Next >= 10]
useRouter
Middleware [Next 12 required]
In my examples a will use a route that requires a reset-token or it should be redirected.
SSR
Firstly server side rending with getServerSideProps.
Vercel recommends to use SSR as a last resort and I would highly recommend not using SSR when able (time to byte & cost).
We suggest trying Incremental Static Generation or Client-side Fetching and see if they fit your needs.
https://vercel.com/blog/nextjs-server-side-rendering-vs-static-generation
But in the case that you do, say there is some server side api validation call you require to validate the query param.
export const getServerSideProps = async (context) => {
const { token } = context.query;
if (!token) {
return {
redirect: {
permanent: false,
destination: "/",
}
}
}
return {
props: {}
// props: { token }
// You could do this either with useRouter or passing props
}
}
useRouter Secondly the easiest useRouter. When I first did this I came across the problem when nextjs/react hydrates there will be a point when the query params are null. Luckily useRouter has isReady!
import Router, { useRouter } from "next/router";
const { query, isReady } = useRouter();
useEffect(() => {
if (!isReady) return;
if (!query.token) {
Router.push("/")
}
}, [isReady])
Middleware now this is my personal favourite as it seperates the functionality in a clean way imo.
I found this based of a vercel example. I would highly recommend reading through a bunch of these to find best practices.
https://github.com/vercel/examples/
import { NextResponse, NextRequest } from 'next/server'
export async function middleware(req) {
const { pathname, searchParams } = req.nextUrl
if (pathname == '/reset-token') {
const index = searchParams.findIndex(x => x.key === "token")
// You could also add token validation here.
if (!index) {
return NextResponse.redirect('/')
}
}
return NextResponse.next()
}
Here is the repo which has some cool filtering of query parameters.
This is a more soft approach instead of hard redirecting.
https://github.com/vercel/examples/tree/main/edge-functions/query-params-filter
Nico also has a great answer on this, expect I wouldn't recommend using hooks like in his example, instead use isReady.
https://stackoverflow.com/a/58182678/4918639
For Class Component Lovers
The even better approach is to listen for a dedicated event for this routeChangeComplete using this.props.router.events.on method, inside componentDidMount if you're using class component -
routeChangeComplete = () => {
// this WILL have valid query data not empty {}
console.log(this.props.router.query);
};
componentDidMount() {
this.props.router.events.on("routeChangeComplete", this.routeChangeComplete);
}
componentWillUnmount() {
this.props.router.events.off("routeChangeComplete", this.routeChangeComplete);
}
Ref: https://nextjs.org/docs/api-reference/next/router#routerevents
routeChangeComplete: Fires when a route changed completely.
Practically when isReady has become true or when router.query object has data.
For NextJS version - 12.0.8
"If you export a function called getServerSideProps (Server-Side Rendering) from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps."
=async functions
refference:https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props#getserversideprops
Simply putting that async function on the page notifies NextJS of its presence.During prerendering stage of the component, the query object of the router will be empty.
isReady: boolean - Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
refference: https://nextjs.org/docs/api-reference/next/router
solution:
import { useRouter } from 'next/router';
const Fn = () =>{
const router = useRouter();
const { param } = router.query;
const fetchData = async () => {
await fetch();
}
useEffect(() => {
fetchCat();
}, [router.isReady]);
}
I resolved my problem that I need it in Hoc component.
I wrapped using withRouter(withLocale(Comp)) and create conditional in HOC
export default function withLocale(WrappedPage) {
const WithLocale = ({ router, ...props }) => {
const { lang } = router.query;
if (!lang || !isLocale(lang)) {
return <Error statusCode={404} />;
}
return (
<LocaleProvider lang={lang}>
<WrappedPage {...props} />
</LocaleProvider>
);
};
return WithLocale;
}
Next.js <= 10.0.5
This is a good work around, I found around from this comment
Add useQuery.ts helper file
// useQuery.js
import { useRouter } from 'next/router';
// Resolves query or returns null
export default function useQuery() {
const router = useRouter();
const ready = router.asPath !== router.route;
if (!ready) return null;
return router.query;
}
usage
// In your components (instead of useRouter)
const query = useQuery();
useEffect(() => {
if (!query) {
return;
}
console.log('my query exists!!', query);
}, [query]);
Class Component | 12/16/2022 | React JS 18.2.0 | Next JS 13.0.6
I got the answer for those who want to use Class Component. This was actually nowhere to be found ! Enjoy !
You will add if(this.props.router.isReady) and include return in the condition in render().
.
.
import { withRouter } from 'next/router';
import { Component } from 'react';
class User extends Component {
...
render() {
if(this.props.router.isReady){ // Add this condition and include return ()
// Do anything
console.log(this.props.router.query) // Log 'query' on first render
return (
<div>
<SearchBar pid={this.props.router.query.pid} /> // Pass the query params to another component if needed
</div>
);
}
}
}
export default withRouter(User);
I'm working on an app that has user authentication. Right now my log in is currently happening in a withAuth HOC, while my Nav component is set up to get the current user.
However, while the use can log in and access their profile page, once they leave profile, current user is empty.
How do I set up my store and/or components so that current user is available on every page.
I tried to pass user to my Nav component, but once you leave profile, there's no user at all
This is my withAuth HOC:
export default function withAuth(WrappedComponent) {
class Something extends React.Component {
componentDidMount() {
**//checks if user is logged in before loading to the WrappedComponent (which in this case is the profile page)**
if (!localStorage.token) {
this.props.history.push("/login")
}
try {
this.props.getCurrentUser()
.catch(error => {
this.props.history.push("/login")
})
} catch (error) {
if (error.message === "Please log in") {
this.props.history.push("/login")
}
}
}
render() {
return (
<WrappedComponent />
)}
}
const mapDispatchToProps = {
getCurrentUser: getCurrentUserAction
**//does a fetch to 'http://localhost:3000/profile' with the bearer token and returns the username**
}
return withRouter(connect(null, mapDispatchToProps)(Something))
}
This is my Nav component (which is imported to every page):
componentDidMount() {
this.props.getCurrentUser();
**//This should fetch from 'http://localhost:3000/profile' with the bearer token and return the username**
}
...
const mapStateToProps = state => {
return {
user: state.user
};
};
const mapDispatchToProps = {
getCurrentUser: getCurrentUserAction,
logout: userLogoutAction
};
I understand why user is visible in profile, but don't understand why it's not visible anywhere else
When HoC component mounts it always mounts the Nav component as well.
Nav component has this.props.getCurrentUser(); which is same thing as this.props.setCurrentUser().
I bet you have some race condition that redirects the user back to login.
I'd suggest you to refactor your code and use redux properly, which in this scenario could be:
getCurrentUserAction handles the request to get user data if user is logged in, then dispatches RECEIVE_USER action, which changes the redux state (by reducer).
HoC makes sure user is logged in and dispatches the action when necessary.
Nav component uses only selector to get the user data it needs (since HoC acts here as a guard).
Above might not be the most optimal solution for you, but it could fix your problem.
I have a simple component that fetches data and only then displays it:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false
stuff: null
};
}
componentDidMount() {
// load stuff
fetch( { path: '/load/stuff' } ).then( stuff => {
this.setState({
loaded: true,
stuff: stuff
});
} );
}
render() {
if ( !this.state.loaded ) {
// not loaded yet
return false;
}
// display component based on loaded stuff
return (
<SomeControl>
{ this.state.stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
);
}
}
Each instance of MyComponent loads the same data from the same URL and I need to somehow store it to avoid duplicate requests to the server.
For example, if I have 10 MyComponent on page - there should be just one request (1 fetch).
My question is what's the correct way to store such data? Should I use static variable? Or I need to use two different components?
Thanks for advice!
For people trying to figure it out using functional component.
If you only want to fetch the data on mount then you can add an empty array as attribute to useEffect
So it would be :
useEffect( () => { yourFetch and set }, []) //Empty array for deps.
You should rather consider using state management library like redux, where you can store all the application state and the components who need data can subscribe to. You can call fetch just one time maybe in the root component of the app and all 10 instances of your component can subscribe to state.
If you want to avoid using redux or some kind of state management library, you can import a file which does the fetching for you. Something along these lines. Essentially the cache is stored within the fetcher.js file. When you import the file, it's not actually imported as separate code every time, so the cache variable is consistent between imports. On the first request, the cache is set to the Promise; on followup requests the Promise is just returned.
// fetcher.js
let cache = null;
export default function makeRequest() {
if (!cache) {
cache = fetch({
path: '/load/stuff'
});
}
return cache;
}
// index.js
import fetcher from './fetcher.js';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false
stuff: null
};
}
componentDidMount() {
// load stuff
fetcher().then( stuff => {
this.setState({
loaded: true,
stuff: stuff
});
} );
}
render() {
if ( !this.state.loaded ) {
// not loaded yet
return false;
}
// display component based on loaded stuff
return (
<SomeControl>
{ this.state.stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
);
}
}
You can use something like the following code to join active requests into one promise:
const f = (cache) => (o) => {
const cached = cache.get(o.path);
if (cached) {
return cached;
}
const p = fetch(o.path).then((result) => {
cache.delete(o.path);
return result;
});
cache.set(o.path, p);
return p;
};
export default f(new Map());//use Map as caching
If you want to simulate the single fetch call with using react only. Then You can use Provider Consumer API from react context API. There you can make only one api call in provider and can use the data in your components.
const YourContext = React.createContext({});//instead of blacnk object you can have array also depending on your data type of response
const { Provider, Consumer } = YourContext
class ProviderComponent extends React.Component {
componentDidMount() {
//make your api call here and and set the value in state
fetch("your/url").then((res) => {
this.setState({
value: res,
})
})
}
render() {
<Provider value={this.state.value}>
{this.props.children}
</Provider>
}
}
export {
Provider,
Consumer,
}
At some top level you can wrap your Page component inside Provider. Like this
<Provider>
<YourParentComponent />
</Provider>
In your components where you want to use your data. You can something like this kind of setup
import { Consumer } from "path to the file having definition of provider and consumer"
<Consumer>
{stuff => <SomeControl>
{ stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
}
</Consumer>
The more convenient way is to use some kind of state manager like redux or mobx. You can explore those options also. You can read about Contexts here
link to context react website
Note: This is psuedo code. for exact implementation , refer the link
mentioned above
If your use case suggests that you may have 10 of these components on the page, then I think your second option is the answer - two components. One component for fetching data and rendering children based on the data, and the second component to receive data and render it.
This is the basis for “smart” and “dumb” components. Smart components know how to fetch data and perform operations with those data, while dumb components simply render data given to them. It seems to me that the component you’ve specified above is too smart for its own good.
I can't get my head wrapped around this.
The problem: let's say there's an app and there can be some sort of notifications/dialogs/etc that i want to create from my code.
I can have "global" component and manage it, but it would limit me to only one notification at a time, this will not fit.
render() {
<App>
// Some components...
<Notification />
</App>
}
Or i can manage multiple notifications by the component Notification itself. But state management will not be clear.
The other problem if i have some sort of user confirmation from that component (if it's a confirmation dialog instead of simple notification) this will not be very convinient to handle with this solution.
The other solution is to render a component manually. Something like:
notify(props) {
const wrapper = document.body.appendChild(document.createElement('div'))
const component = ReactDOM.render(React.createElement(Notification, props), wrapper)
//...
// return Promise or component itself
}
So i would call as:
notify({message: '...'})
.then(...)
or:
notify({message: '...', onConfirm: ...})
This solution seems hacky, i would like to let React handle rendering, and i have an additional needless div. Also, if React API changes, my code breaks.
What is the best practice for this scenario? Maybe i'm missing something completely different?
You could use React Context for this.
You create a React context at a high level in your application and then associate a values to it. This should allow components to create / interact with notifications.
export const NotificationContext = React.createContext({
notifications: [],
createNotification: () => {}
});
class App extends Component {
constructor() {
super();
this.state = {
notifications: []
};
this.createNotification = this.createNotification.bind(this);
}
createNotification(body) {
this.setState(prevState => ({
notifications: [body, ...prevState.notifications]
}));
}
render() {
const { notifications } = this.state;
const contextValue = {
notifications,
createNotification: this.createNotification
};
return (
<NotificationContext.Provider value={contextValue}>
<NotificationButton />
{notifications.map(notification => (
<Notification body={notification} />
))}
</NotificationContext.Provider>
);
}
}
The notifications are stored in an array to allow multiple at a time. Currently, this implementation will never delete them but this functionality can be added.
To create a notification, you will use the corresponding context consumer from within the App. I have added a simple implementation here for demonstration purposes.
import { NotificationContext } from "./App.jsx";
const NotificationButton = () => (
<NotificationContext.Consumer>
{({ notifications, createNotification }) => (
<button onClick={() => createNotification(notifications.length)}>
Add Notification
</button>
)}
</NotificationContext.Consumer>
);
You can view the working example here.
I'm trying to use the new React context to hold data about the logged-in user.
To do that, I create a context in a file called LoggedUserContext.js:
import React from 'react';
export const LoggedUserContext = React.createContext(
);
And sure enough, now I can get access to said context in other components using consumers, as I do here for example:
<LoggedUserContext.Consumer>
{user => (
(LoggedUserContext.name) ? LoggedUserContext.name : 'Choose a user or create one';
)}
</LoggedUserContext.Consumer>
But obviously, for this system to be useful I need to modify my context after login, so it can hold the user's data. I'm making a call to a REST API using axios, and I need to assign the retrieved data to my context:
axios.get(`${SERVER_URL}/users/${this.state.id}`).then(response => { /*What should I do here?*/});
I see no way to do that in React's documentation, but they even mention that holding info of a logged in user is one of the use cases they had in mind for contexts:
Context is designed to share data that can be considered “global” for
a tree of React components, such as the current authenticated user,
theme, or preferred language. For example, in the code below we
manually thread through a “theme” prop in order to style the Button
component:
So how can I do it?
In order to use Context, you need a Provider which takes a value, and that value could come from the state of the component and be updated
for instance
class App extends React.Component {
state = {
isAuth: false;
}
componentDidMount() {
APIcall().then((res) => { this.setState({isAuth: res}) // update isAuth })
}
render() {
<LoggedUserContext.Provider value={this.state.isAuth}>
<Child />
</LoggedUserContext.Provider>
}
}
The section about dynamic context explains it
Wrap your consuming component in a provider component:
import React from 'react';
const SERVER_URL = 'http://some_url.com';
const LoggedUserContext = React.createContext();
class App extends React.Component {
state = {
user: null,
id: 123
}
componentDidMount() {
axios.get(`${SERVER_URL}/users/${this.state.id}`).then(response => {
const user = response.data.user; // I can only guess here
this.setState({user});
});
}
render() {
return (
<LoggedUserContext.Provider value={this.state.user}>
<LoggedUserContext.Consumer>
{user => (
(user.name) ? user.name : 'Choose a user or create one';
)}
</LoggedUserContext.Consumer>
</LoggedUserContext.Provider>
);
}
}
I gave a complete example to make it even clearer (untested). See the docs for an example with better component composition.