react-router : share state between Routes without Redux - reactjs

I would like to have a shared state (list of clients fetched remotely) between 2 sibling Routes : Timesheets and Clients.
I want to try how far i can go with 'pure' React (No Flux architecture).
This example works, but I have an error : browser.js:49 Warning: [react-router] You cannot change <Router routes>; it will be ignored
So, it doesn't seem to like async props.
constructor(props) {
super(props);
this.state = {
clients : []
}
}
componentDidMount() {
fetch("clients.json")
.then(response => response.json())
.then(clients => this.setState({ clients }));
}
render() {
return (
<Router history={browserHistory}>
<Route path="/" component={Header} >
<Route path="timesheet" component={() => (<Timesheets {...this.state} />) }/>
<Route path="clients" component={() => (<Clients {...this.state} />) }/>
</Route>
</Router>
);
}
Is it possible to send async props down to each route?
Or is it possible to set the whole state in the parent route (Header component) and then access this state from each child route (Timesheets and Clients components)?

You can use an high-order component to fetch and inject data to your top level component. Then you can pass props to sub routes via React.cloneElement.
HOC
const FetchHOC = url => Component => class extends React.Component() {
state = { data: null };
componentDidMount() {
fetch(url).then(data => this.setState({ data }));
}
render() {
return <Component {...this.props} {...this.state.data} />;
}
}
Route configuration
<Route path="/" component={FetchHOC('http://some-url')(App)}>
<Route path="subroute1" component={SubRoute1} />
<Route path="subroute2" component={SubRoute2} />
</Route>
Parent route render()
<div className="App">
{this.props.children && React.cloneElement(this.props.children, {
data: this.props.data,
// ...
})}
</div>

You can take a look at reacts Context. Redux also makes use of Context. It allows you to pass some data down to all children. But you should write code to use this data, for instance you have determine contextTypes etc.
You can see details on docs about how to use it.

Related

Render state in another component in reactjs

I'm trying to display current user name in a different component but It's not working, what am I doing wrong ?
Here's where I'm initialising the username (App.js)
class App extends Component{
constructor(props) {
super(props);
this.state = {
logged_in: localStorage.getItem('token') ? true : false,
username: ''
};
}
componentDidMount() {
if (this.state.logged_in) {
fetch('http://localhost:8000/user/current_user/', {
headers: {
Authorization: `JWT ${localStorage.getItem('token')}`
}
})
.then(res => res.json())
.then(json => {
this.setState({ username: json.username });
console.log(json.username);
});
}
}
render() {
return (
<div className="App">
<Router history={history}>
<Switch>
<Route exact path="/" component={First} />
<Route path="/home" component={Projects} />
<Route path="/menu" component={MenuH} />
<Route path ="/createProject" component={CreateP} />
<Route path ="/connect" component={Newacount} />
<Route component={Notfound} />
</Switch>
</Router>
</div>
);
}
}
...
And here's how I'm calling it in a different component (projects)
{this.state.username}
Here's a good discussion about How to pass component via react router.
When following the discussion, you code will look like:
<div className="App">
<Router history={history}>
<Switch>
<!-- Pass username as props here -->
<Route path="/your-route" render={()=><Projects username={this.state.username}/>} />
</Switch>
</Router>
</div>
And into your Projects component, you retrieve it with this.props.username.
Your Projects component does not have access to the state in App.
You should do this with a state manager like React Context or Redux
This won't work, you cannot just access the parent's state directly in all the child components,
you have to send the parents state down to the child components as a prop to access them.
In scenario like this where you want to use one state in all of your child components the good thing is to use Redux, it will provide you a global state which will be accessible in all of your components.
Also you can solve this problem with React's Context API.
try sending state as props for child and access it from child! or to access state directly u can use redux store or context api from react!
<Route path="/home" render={()=><Home username={this.state.username} />} />

react.js redirect to view

i want redirect to "/user". i write but this not work.
how to correctly redirect to the right page
onClick = (e) => {
this.setState({ errorLoad: false});
getPlayerInfo(this.state.id).then(data => {
if(data.success == false) {
this.setState({ errorLoad: true});
return;
}
this.setState({ user: data.player});
console.log(data);
<Redirect to="/user"/>
});
}
My router list. Among them there is a router with the path "/ user"
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
UPADATE
App.js
The button I click on is in the component <SearchForm/>
render() {
let style = {marginLeft: '20px'};
return (
<div>
<Header source='https://www.shareicon.net/data/2017/02/15/878753_media_512x512.png'/>
<SearchForm onClick={this.onClick} style={style} onChange={this.onHandle} placeholder="search"/>
<Centered style={ {marginTop: '50px'} }>
<BrowserRouter>
<Switch>
<Route exact path='/' component={Startup} />
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
</Switch>
</BrowserRouter>
</Centered>
</div>
);
}
There are two ways to programmatically navigate with React Router - <Redirect /> and history.push. Which you use is mostly up to you and your specific use case.
<Redirect /> should be used in user event -> state change -> re-render order.
The downsides to this approach is that you need to create a new property on the component’s state in order to know when to render the Redirect. That’s valid, but again, that’s pretty much the whole point of React - state changes update the UI.
The real work horse of React Router is the History library. Under the hood it’s what’s keeping track of session history for React Router. When a component is rendered by React Router, that component is passed three different props: location, match, and history. This history prop comes from the History library and has a ton of fancy properties on it related to routing. In this case, the one we’re interested is history.push. What it does is it pushes a new entry onto the history stack - aka redirecting the user to another route.
You need to use this.props.history to manually redirect:
onClick = (e) => {
this.setState({ errorLoad: false});
getPlayerInfo(this.state.id).then(data => {
if(data.success == false) {
this.setState({ errorLoad: true});
return;
}
this.setState({ user: data.player});
console.log(data);
this.props.history.push('/user');
});
}
You should be getting history as a prop from your <Router> component.
EDIT:
Okay thank you for the code update. The SearchForm component is not nested under your BrowserRouter, so it is not getting the history prop. Either move that component inside the BrowserRouter or use the withRouter HOC in SearchForm reacttraining.com/react-router/web/api/withRouter
Option 1: Move SearchForm inside the BrowserRouter
render() {
let style = {marginLeft: '20px'};
return (
<div>
<Header source='https://www.shareicon.net/data/2017/02/15/878753_media_512x512.png'/>
<Centered style={ {marginTop: '50px'} }>
<BrowserRouter>
<SearchForm onClick={this.onClick} style={style} onChange={this.onHandle} placeholder="search"/>
<Switch>
<Route exact path='/' component={Startup} />
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
</Switch>
</BrowserRouter>
</Centered>
</div>
);
}
Option 2: use the withRouter HOC to inject the history prop into SearchForm manually:
import { withRouter } from 'react-router-dom';
class SearchForm extends React.Component { ... }
export default withRouter(SearchForm)

Keeping State After Component Unmounts

I have an Auth component which is responsible for both login & sign up. I simply receive a prop (isSignup) and display the appropriate form fields. I also use react-router:
<BrowserRouter>
<Route
path="/signup" exact
render={() => <Auth isSignup />} />
<Route
path="/login" exact
render={() => <Auth />} />
</BrowserRouter>
In the Auth component I have a state which holds the values of the form fields.
I'd like to keep the state of Auth after it unmounts, i.e when react-router no longer renders it, so I can keep the values when the user switches between /signup and /login.
Is there a way to do this without global state management (e.g Redux)?
If you don't want to use Redux for this purpose, but you still want to share this specific state across your app, then there are a few ways to go about it. If you want to stay "Reacty", then you really have 2 options:
Pass state as props
Use a Provider
If you just want something that works but isn't necessarily the React way, you have more options (like setting window.isLoggedIn and toggling it at will... which I don't recommend).
For this answer, we'll focus on the "Reacty" options.
Pass state as props
This is the default approach with React. Track your isLoggedIn variable somewhere at the root of your component tree, then pass it down to your other components via props.
class App extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
render() {
return (
<BrowserRouter>
<Route
path="/signup"
exact
render={() => (
<Auth
isSignup
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
<Route
path="/login"
exact
render={() => (
<Auth
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
</BrowserRouter>
)
}
}
This works but gets tedious as you need to pass isLoggedIn, logIn, and logOut to almost every component.
Use a Provider
A Provider is a React component that passes data to all its descendants via context.
class AuthProvider extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
getChildContext() {
return {
isLoggedIn: this.state.isLoggedIn,
logIn: this.logIn,
logOut: this.logOut
}
}
static childContextTypes = {
isLoggedIn: PropTypes.bool,
logIn: PropTypes.func,
logOut: PropTypes.func
}
render() {
return (
<>
{this.props.children}
</>
)
}
}
Then, you initialize your app by wrapping the whole thing in AuthProvider:
<AuthProvider>
<BrowserRouter>
// routes...
</BrowserRouter>
</AuthProvider>
And in any component of your app, you will be able to access isLoggedIn, logIn, and logOut via this.context.
You can read more about the Provider pattern and how it works in this excellent article from Robin Wieruch.
notes:
It's common for components to receive context as props using a higher order component, but I left that out of the answer for brevity. Robin's article goes into this further.
You may recognize the Provider pattern as one that react-redux and other state management libraries utilize. It's fairly ubiquitous at this point.
You could create a wrapper which stores the state above the components:
class AuthWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return ([
<Route
key="signup"
path="/signup" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
isSignup
/>
</Route>,
<Route
key="login"
path="/login" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
/>
</Route>
]);
}
}
Then this component can be used, which stores the state over those routes.
<BrowserRouter>
<AuthWrapper />
</BrowserRouter>
You would just need to use the state and setState in the params instead in the Auth component.

How to re-render the same component being used in different routes?

I have several routes rendering the same component. Depending on the route I want the component to fetch different data. However since I keep rendering the same component, React doesn't see any changes to the DOM when I click a Link tag (from my nav bar located in the Layout component) to another route rendering that same component. Meaning the component is not re-rendered with the new data. Here are my routes:
class App extends Component {
render() {
return (
<BrowserRouter>
<Provider store={store}>
<Layout>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/fashion" component={PostTypePageContainer} />
<Route exact path="/beauty" component={PostTypePageContainer} />
</Switch>
</Layout>
</Provider>
</BrowserRouter>
);
}
}
export default App;
Here is the PostTypePageContainer component that I want to re-render with the new data each time:
class PostTypePageContainer extends Component {
componentDidMount() {
let route;
switch (this.props.location.pathname) {
case '/fashion':
route = '/fashion';
break;
case '/beauty':
route = '/beauty';
break;
default:
console.log('No data was found');
}
let dataURL = `http://localhost:8888/my-site//wp-json/wp/v2${route}?_embed`;
fetch(dataURL)
.then(res => res.json())
.then(res => {
this.props.dispatch(getData(res));
});
}
render() {
let posts = this.props.postData.map((post, i) => {
return <PostTypePage key={i} props={post} />;
});
return <div>{posts}</div>;
}
}
const mapStateToProps = ({ data }) => ({
postData: data.postData
});
export default connect(mapStateToProps)(PostTypePageContainer);
How do I go about re-rendering that component each time?
This is intended behavior of react-router.
While i suggest you create a HOC to fetch the data from different locations and pass it to the PostTypePageContainer via props, using a key will give you a quick work around that will cause your component to remount.
class App extends Component {
render() {
return (
<BrowserRouter>
<Provider store={store}>
<Layout>
<Switch>
<Route exact path="/" component={Home} />
<Route exact key={uniqueKey} path="/fashion" component={PostTypePageContainer} />
<Route exact key={someOtherUniqueKey} path="/beauty" component={PostTypePageContainer} />
</Switch>
</Layout>
</Provider>
</BrowserRouter>
);
}
}
export default App;
Source: https://github.com/ReactTraining/react-router/issues/1703
I wasn't able to get the <Route key={...} ... /> to work in my case. After trying several different approaches the one that worked for me was using the componentWillReceiveProps function in the reused component. This was getting called each time the component was called from a <Route />
In my code, I did:
componentWillReceiveProps(nextProps, nextContext) {
// When we receive a call with a new tag, update the current
// tag and refresh the content
this.tag = nextProps.match.params.tag;
this.getPostsByTag(this.tag);
}

high order component throws an invalid react element error (reactjs)

So I've been reading about HOCs lately and decided to use them in my application to pass down authorisation logic to the child components.
I'm trying to render a <Route /> component through the HOC but it logs the error:
Uncaught Error: AuthRoute(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.
Here's the code for the HOC:
const AuthRoute = ({ component: Component }) => {
class AuthComponent extends Component {
// Authorisation logic here
render() {
return (
<Route render={props => <Component {...props}/>} />
)
}
}
return AuthComponent;
};
And then I'm using this HOC as an alias of <Route /> component like this:
<BrowserRouter>
<AuthRoute path="/account" component={PrivateComponent} />
</BrowserRouter>
EDIT:
But this approach works fine:
const AuthRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
checkAuth() ? (<Component {...props}/>) : (<Redirect to={{pathname: '/', state: { from: props.location }}}/>)
)}/>
);
<BrowserRouter>
<AuthRoute path="/account" component={PrivateComponent} />
</BrowserRouter>
You need to review your architecture . Indeed, Your approach is totally contradictory regarding HOC pattern . You may need to do the following instead :
<BrowserRouter>
<Route path="/account" component={AuthRoute(PrivateComponent)} />
</BrowserRouter>
If you agree with this design of calling the HOC, The HOC implementation will be :
const AuthRoute = (Composed) => {
class AuthComponent extends React.Component {
// Authorisation logic here
componentWillMount() {
if (!checkAuth()) {
this.props.history.push({pathname: '/', state: { from: this.props.location }});
}
}
render() {
return (
<Composed {...this.props} />
)
}
}
return AuthComponent;
};
In the first case you are returning a class instance
const AuthRoute = ({ component: Component }) => {
class AuthComponent extends Component {
// Authorisation logic here
render() {
return (
<Route render={props => <Component {...props}/>} />
)
}
}
return AuthComponent; // returning the class object here and not an instance
};
So if your wish to use it you would need to write
<BrowserRouter>
<Route path="/account" component={AuthRoute(PrivateComponent)} />
</BrowserRouter>
where AuthRoute(PrivateComponent) is a class object and Route creates an instance out of it internally
However in the second case, its not an HOC, but a functional component that returns a valid React Element,
const AuthRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
checkAuth() ? (<Component {...props}/>) : (<Redirect to={{pathname: '/', state: { from: props.location }}}/>)
)}/>
);
and hence
using <AuthRoute path="/account" component={PrivateComponent} /> , you called a component instance whereby props path and component are received by the functional component.
I’m not very familiar with the syntax you wrote. But spotted one thing. Your parameter is component but inside you are using Component which in this context is undefined
It should be :
const AuthRoute = (Component ) => { // <-- Component which refers to PrivateComponent
class AuthComponent extends React.Component { // <-- React.Component
....
and Not :
const AuthRoute = ({ component: Component }) => {
class AuthComponent extends Component {
...
Also check out my other answer to have elegant HOC .

Resources