I'm using react-router 1.0 and react-redux on an app, and I'm wondering what strategy is best to pass props to children on larger apps. Here's a basic situation:
Let's say I have a route /admin/users/edit/:id with the following structure on its components:
Routes:
<Router>
<Route path="admin" component={Admin}>
<Route path="users" component={Users}>
<Route path="edit/:id" component={Edit}/>
</Route>
</Route>
</Router>
Admin:
class Admin extends React.Component {
render() {
return (
{this.props.children}
)
}
}
Users:
class User extends React.Component {
edit = (id, params) => {
const { dispatch } this.props;
dispatch(edit(id, params));
}
other functions (add, remove) ...
render() {
return (
{this.props.children}
)
}
}
function mapStateToProps(state) {
const { users } = state;
return { users };
}
export default connect(mapStateToProps)(User);
Edit:
class Edit extends React.Component {
submit () => {
const { id } = this.props.params;
const { firstName } = this.refs;
this.props.edit(id, {firstName: firstName.value});
}
render() {
const { id } = this.props.params;
const user = this.props.users[id];
return (
<form>
<input ref='firstName' defaultValue={user.firstName}/>
<button onClick={this.submit}>Submit</button>
</form>
)
}
}
How would I pass the users & edit function props down to the children?
I know about React.cloneElement() (as in https://github.com/rackt/react-router/tree/master/examples/passing-props-to-children), but if I have multiple routes like /users/add, /users/remove/:id, etc, I would be passing and exposing all the functions (edit, add, remove...) to all children. That solution doesn't seem to work very well when you have more than one children.
I would like to keep my children as dumb as possible, and use this same structure across the application (/images/add, /images/remove/:id, etc).
Any suggestions?
You have a few options:
First level children:
Use React.cloneElement(), that's something you are already aware of. I wouldn't use it for deeply nested Components though.
To all routes:
Use createElement():
<Router createElement={createElement} />
// default behavior
function createElement(Component, props) {
// make sure you pass all the props in!
return <Component {...props}/>
}
// maybe you're using something like Relay
function createElement(Component, props) {
// make sure you pass all the props in!
return <RelayContainer Component={Component} routerProps={props}/>
}
Check more on that in the React Router docs.
Use context:
Note:
Context is an advanced and experimental feature. The API is likely to change in future releases.
See how to use it in the React docs.
There is also the whole thread about it in the react-router#1531.
Related
I'm using TypeScript with React and I want to pass a userId prop to every component rendered.
I don't want to add userId to props for every single component and can't figure out how to make every React component have the prop.
To make it all even harder, right now it's giving me a TS error JSX element type 'Component' does not have any construct or call signatures.
How would I make this all work?
type Props = {
children: JSX.Element;
// I have tried many different things, but nothing worked...
// React.ComponentType<{ user: PartialUser }>;
};
type PartialUser = {
id: number;
username: string;
email: string;
};
const PrivateRoute = ({ children, ...rest }: Props) => {
const { data, error, loading } = useMeQuery();
let me;
let user;
if (data?.me) {
me = data!.me!;
user = {
id: me.id,
username: me.username,
email: me.email,
};
}
if (error) console.error(error);
const Component = children;
return (
<>
{!loading && user ? (
<Route {...rest}>
<Component user={user} />
</Route>
) : (
<Redirect to="/login" />
)}
</>
);
};
Well the other way you can do is using Context API. Have a look at https://reactjs.org/docs/context.html#when-to-use-context for more understanding
What I would do is I would create a custom type that would contain the user id and then use this type to create react context.
Now next step would be use this context and create a provider and then render necessary components inside this context provider. You can then consume this context in any of the child component that is at any deep level nested in this custom created context and you can get the user id.
For eg.
Create custom context type and react context:
export type MyContextType = {
userId: number
}
export const MyContext = React.createContext<MyContextType>(undefined!);
Now use provider and pass the initial value in your main/root file
const context: MyContextType = {
userId: 1 (I am assuming you would get this from API response or local storage)
};
<MyContext.Provider value={context}>
...nested components
<MyComponent />
</MyContext.Provider>
and then in any of your nested component you can get this context and the value using:
class MyComponent extends React.Component<MyComponentProps, State> {
static contextType = MyContext;
context!: React.ContextType<typeof MyContext>;
render() {
console.log(this.context.userId); // You should be able to see 1 being printed in the console.
}
}
Component vs. Element
There is a difference between passing the component itself as the children prop:
<PrivateRoute>
{MyComponent}
</PrivateRoute>
and passing an instance of the component:
<PrivateRoute>
<MyComponent/>
</PrivateRoute>
The type children: JSX.Element applies to the second case and that is why you get the error:
JSX element type 'Component' does not have any construct or call signatures.
When you have already called the component through JSX like <MyComponent/> then you just get a JSX.Element which is not callable.
Typing the Component
You want to be passing a callable component. If this is react-router-dom I would recommend using the component prop instead of the children prop since they behave differently.
We want to say that our component can take the standard RouteComponentProps and also a user prop.
The PrivateRoute itself should take all of the RouteProps like to, exact, etc. But we will require a component prop with our modified type.
In order to call the component with our extra prop, we could use an inline render function render={props => <Component {...props} user={user} />}. We need to destructure it with an uppercase name in order to call it. We could also move the authentication logic into a higher-order component and use component={withUser(component)}.
Other Issues
We are going to be including this PrivateRoute alongside other Route components. So we want to render the Redirect only when we are actually on this Route. In other words, it should be a replacement of the component rather than a replacement of the Route. Otherwise traffic to other non-private routes will get inadvertently redirected.
You probably don't want to Redirect to the login page until after loading has finished. You can render a loading spinner or nothing while waiting for completion.
I'm not sure why there is so much checking and assertion on user. You should be able to do const user = data?.me; and get either a PartialUser or undefined.
Code
import React from "react";
import { Redirect, Route, RouteComponentProps, RouteProps, Switch } from "react-router-dom";
type PartialUser = {
id: number;
username: string;
email: string;
};
declare function useMeQuery(): { error: any; loading: boolean; data?: { me: PartialUser } }
type ComponentProps = RouteComponentProps & {
user: PartialUser;
}
type Props = RouteProps & {
component: React.ComponentType<ComponentProps>;
// disallow other methods of setting the render component
render?: never;
children?: never;
};
const PrivateRoute = ({ component: Component, ...rest }: Props) => {
const { data, error, loading } = useMeQuery();
const user = data?.me;
return (
<Route
{...rest}
render={(props) => {
if (user) {
return <Component {...props} user={user} />;
} else if (loading) {
return null; // don't redirect until complete
} else {
return <Redirect to="/login" />;
}
}}
/>
);
};
const MustHaveUser = ({user}: {user: PartialUser}) => {
return <div>Username is {user.username}</div>
}
export const Test = () => (
<Switch>
<Route path="/home" render={() => <div>Home</div>} />
<PrivateRoute path="/dashboard" component={MustHaveUser} />
</Switch>
)
Typescript Playground Link
I'd do it like this:
types.ts
export interface Propys{
userId:string;
}
MyComponent.tsx
import { Propys } from "./types";
const MyComponent:React.FC<Propys> = ({userId}) => {
return <>userId</>;
}
I use reach router like this below before and it works fine
.....
<Router>
<ComponentA path="/:id">
<ComponentB path="/">
<Router>
....
I decided to refactor my code with context, and the code is refactored to something like this:
<GlobalContextProvider>
<GlobalContext.Consumer>
{( context) =>{
return(
.....
<Router>
<ComponentA path="/:id">
<ComponentB path="/">
<Router>
....
}
After the refactor, the ComponentA is not working properly, as the url param prop id is not passed
In the ComponentA.js , test like this:
componentDidMount() {
const { id } = this.props;
console.log(id); // return undefined
}
Also when I console.log(this.props) , it returns the same result as this.context
Can someone help me understand why this is happening? How to refactor with context properly?
Thanks a lot
I finally figure out this issue:
ComponentA is wrapped with HOC, and by adding {...this.props} to ComposedComponent,
ComponentA can access the url params from this.props
Please refer this issue Passing React context through an HOC to a wrapped component
I'm not sure it was working at first.
To access the param value, you have to do it this way :
const { match } = props;
const { params } = match;
const { id } = params;
You might need to wrap your component into withRouter(...)
import { withRouter } from "react-router-dom";
class MyComp extends PureComponent {...}
export default withRouter(MyComp);
I'm working on a React app where the user needs to be logged-in to do anything. This means that by default every route requires authentication, expect the few pages needed to create an account and so on.
Every article or tutorial I found on the subject (How to implement authenticated routes in React Router 4?) explains how to put all your private pages behind one route (usually "dashboard/"). But I don't want to artificially force my application to have this route structure. When I used to work with AngularJS, I would specify for each route if the user needs to be authenticated or not to access it.
So what's currently the best way to structure your router in react to specify that a few routes are publicly accessible, and the others require authentication and redirect you to the login page if you are not?
I agree that the solution is with a high order component, here is another example to avoid asking on each route and have a more general way to make private a page
You have a wrapper component: withAuthorization that wraps your component to check if you have access or no to that content.
This is just a quick example, I hope it can helps you
const withAuthorization = Component => {
return class WithAuthorization extends React.Component {
constructor(props){
super(props);
this.state = {
auth: false
}
}
async componentDidMount() {
// ask in your api for the authorization
// if user has authorization
this.setState({ auth: true })
}
render () {
const { auth } = this.state;
return (
{auth ? <Component {...this.props} /> : <Redirect to={`login`} />}
)
}
}
}
export default withAuthorization;
Then when you export your components just have to do it in this way:
withAuthorization(ComponentToExport)
Essentially you can create a Higher Order Component that use can use to check the auth and do what is necessary... I do something like this for my protected routes:
export const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) =>
checkAuth(user) === true ? (
<Component {...props} />
) : (
<Redirect to="/auth/login" />
)
}
/>
);
};
There are several ways to pass in your user object...so I have not put that in there
then in my router I use it as follows:
<PrivateRoute
exact
path="/application/version"
component={AppVersion}
/>
I'm creating a code splitting solution for react app using react router, webpack and dynamic imports. The idea is to map all the routes and corresponding components to different app contexts and split code according to app contexts. When user visits some route the whole code chunk of related app context is being loaded.
Code examples:
App.tsx:
class App extends React.Component<Props, State> {
render() {
if (this.props.data.loading || this.props.localData.loading) {
return <Loader />
}
return (
<IntlProvider locale={this.props.localData.session.locale} messages={this.state.translations}>
<Router history={history}>
<Route
exact
path={`/screens/:action?`}
render={() => <ComponentLoader contextName={Context.Screens} componentName={'ScreenList'} />}
/>
</Router>
</IntlProvider>
)
}
}
export default withData(App)
ComponentLoader.tsx:
export enum Context {
Screens = 'Screens',
Channels = 'Channels'
}
const CONTEXT_LOADERS: { [name: string]: ComponentChunkLoader } = {
[Context.Screens]: () => import('../../routerContexts/screens'),
[Context.Channels]: () => import('../../routerContexts/channels'),
}
const loadedContexts: ContextsCollection = {}
class ComponentLoader extends React.PureComponent<Props, State> {
state: State = {
Component: null
}
async componentDidMount() {
this._updateComponent(this.props)
}
async componentDidUpdate(prevProps: Props, prevState: State) {
if (this.props.componentName !== prevProps.componentName || this.props.contextName !== prevProps.contextName) {
this._updateComponent(this.props)
}
}
_updateComponent = async (props: Props) => {
let module = loadedContexts[props.contextName]
? loadedContexts[props.contextName]
: await CONTEXT_LOADERS[props.contextName]()
if (!loadedContexts[props.contextName]) loadedContexts[props.contextName] = module
let ComponentClass = module[props.componentName]
this.setState({
Component: ComponentClass
})
}
render() {
if (this.state.Component !== null) {
return <this.state.Component />
}
return <Loader />
}
}
export default ComponentLoader
So when I switch to different routes, I see all the components correctly, and code splitting works correctly.
The problem is: when the data in react context updates, ScreenList component in the example doesn't get updated. When I pass ScreenList directly to react router Route everything works well. So the problem is in my ComponentLoader component that I use for code splitting.
Any ideas what can be the reason?
UPDATE:
Alrite, what i figured out now: if I wrap my ComponentLoader in a HOC that injects some data from context (like export default withRouter(ComponentLoader)) , everything works well and components rerender as expected. Why is happening like this?
I have an initial redux state like this:
{
loggedInUserId: null,
comments: []
}
Here's how my React App looks like:
class App extends Component {
componentWillMount() {
this.props.getLoggedInUserId();
}
render() {
return (
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/comments" component={Comments} />
</Switch>
);
}
}
In my App, I dispatch an action getLoggedInUserId() which asynchronously fills the loggedInUserId in the state.
The HomePage is a dumb component showing some text. I start the app (route is now '/'), see the HomePage component, then I navigate to the Comments page, which has:
componentWillMount() {
this.props.fetchComments(this.props.loggedInUserId); // Dispatch action to do API call to fetch user's comments
}
render() {
// Show this.props.comments nicely formatted
}
Everything works, I see the list of comments in the Comments component.
But if I refresh the page on the route /comments, then by the time the Comments runs componentWillMount, the loggedInUserId has not been loaded yet, so it will call fetchComments(null).
Right now, to fix this, I'm doing in my Comments component:
componentWillMount() {
if (!this.props.loggedInUserId) return;
this.props.fetchComments(this.props.loggedInUserId);
}
componentWillReceiveProps(nextProps) {
if (!this.props.loggedInUserId && nextProps.loggedInUserId) {
nextProps.fetchComments(nextProps.loggedInUserId);
}
}
which works well. But I'm doing this in 10+ components, and it seems like a lot of work which can be factorized, but I didn't find an elegant way to do it.
So I'm asking you how do you generally deal with this kind of situation? Any idea is welcome:
HOC
side-effects
other libraries
I'm using wrapper around Route, which checks if users are logged in and if not, redirect them to login page. Wrapped routes are rendered only after userId of authenticated user is fetched.
import * as React from 'react'
import { Route, Redirect } from 'react-router-dom'
import URLSearchParams from 'url-search-params'
class AuthRoute extends React.Component {
componentDidMount() {
if (!this.props.isLoading) {
this.props.getLoggedInUserId()
}
}
render() {
if (this.props.isLoading) {
// first request is fired to fetch authenticated user
return null // or spinner
} else if (this.props.isAuthenticated) {
// user is authenticated
return <Route {...this.props} />
} else {
// invalid user or authentication expired
// redirect to login page and remember original location
const search = new URLSearchParams({
next: this.props.location.pathname,
})
const next =
this.props.location.pathname !== '/' ? `?${search.toString()}` : ''
return <Redirect to={`/login${next}`} />
}
}
}
You need to update your reducer which handle getLoggedInUserId action to store also isLoading state.
You probably want the initial state to be rendered by the server into 'index.html' (or what have you) and hydrated on the client.
This initial state would include loggedInUserId and data for the /comments page.
Check out https://redux.js.org/docs/recipes/ServerRendering.html
I think using HOC will be clean here. As all the common logic will be at the same place. Use composition here
Let say you have components A, B, C, D
Now you want to write some common function on the componentWillReceiveProps lifecycle of all the components.
Write a HOC like:
class HOC extends React.Component {
componentWillReceiveProps(nextProps) {
//Your commomn logic
}
render() {
const childrenWithProps = React.Children.map(this.props.children,
child => React.cloneElement(child, {
...this.props,
})
return (
<div>
{childrenWithProps}
</div>
)
}
}
Write your components like this:
class A extends React.Component {
componentWillReceiveProps(nextProps) {
//your uncommone logic
}
render(){
return (
<HOC {...this.props}>
<div>
//Your page jsx
</div>
</HOC>
)
}
}
same way write for component B, C, and D. This pattern is useful when there is lot common among components. So better have a look at your usecase
OP writing. After reading nice ideas here, I decided to go with a custom HOC:
import React, { Component } from 'react';
const requireProp = (As, propsSelector, propsToDispatch) =>
class Wrapper extends Component {
componentWillMount() {
if (!propsSelector(this.props) && typeof propsToDispatch === 'function') {
propsToDispatch(this.props);
}
}
render() {
const { ...props } = this.props;
return !!propsSelector(this.props) && <As {...props} />;
}
};
export default requireProp;
To see how I use it, see this gist.