React routes not automatically updating when nested under 'smart' Redux container components - javascript

I'm trying to create an Electron app using React, React-router and Redux. What I'm finding is that my routing logic works absolutely fine when I'm nesting the switch/route logic under a purely presentational component (Page), but that I'm forced to refresh the page to see navigational changes if nested under a 'smart' container component.
Near the top of my React component hierarchy (right beneath HashRouter) I have a Page:
export default function Page (props) {
return (
<div className={`${styles.page}`}>
<SideBar/>
<DetailPane>{props.children}</DetailPane>
</div>
);
}
Here, DetailPane and SideBar are both container components wrapped around presentational components of the same name.
At startup (and during hot reloads), I create my React hierarchy using this function:
export default () => (
<Router>
<Page>
<Switch>
<Route exact path='/txDefinitions/:definitionName/:fieldName' component={FieldPage}/>
<Route exact path='/txDefinitions/:definitionName?' component={DefinitionPage}/>
<Route exact path='/rxDefinitions/:definitionName?' component={DefinitionPage}/>
<Route exact path='/'/>
<Route component={Route404}/>
</Switch>
</Page>
</Router>
This means that <Switch>...</Switch> gets nested underneath <DetailPane>.
If I try to navigate around my app (clicking links in the side bar), I won't actually see the detail pane render the new component until I force-reload the Electron app.
However, I find that routing works as expected if I omit DetailPane from Page:
export default function Page (props) {
return (
<div className={`${styles.page}`}>
<SideBar/>
{props.children}
</div>
);
}
Here is my React hierarchy without DetailPane (works fine):
Here is my React hierarchy with DetailPane (does not work right):
(Apologies for using images but I'm not sure if there's a way to copy from React devtools into clipboard - appears larger if opened in a new tab).
As I was writing this question, I realised this wouldn't be a huge issue for me because earlier refactoring had made the 'smart' version of DetailPane apparently obsolete. Using the purely presentational version of DetailPane
instead resolves this issue:
import * as React from 'react';
//import {DetailPane} from '../../containers'; // Smart/Redux
import {DetailPane} from '../../components'; // Dumb/presentational
import {SideBar} from '../../containers/';
const styles = require('./Page.scss');
export default function Page (props) {
return (
<div className={`${styles.page}`}>
<SideBar/>
<DetailPane>{props.children}</DetailPane>
</div>
);
}
However, I'm still curious why this doesn't work for the container component version. For reference, this is the container component version of DetailPane:
import {connect} from 'react-redux';
import {DetailPane} from '../../components';
// TODO: delete this container?
function mapStateToProps (state): {} {
return {};
}
function mapDispatchToProps (dispatch) {
// TODO.
return {};
}
export default connect(mapStateToProps, mapDispatchToProps)(DetailPane);

The connect HOC implements shouldComponentUpdate logic so if the props don't change, the component doesn't update.
To prevent this from occurring, and have the component always render, you can override the pure option in the connect call.
export default connect(mapStateToProps, mapDispatchToProps, undefined, { pure: false })(DetailPane);
See the react-redux API docs for more details.

Related

Is this the correct way of using React.Suspense?

I have several questions about React.Suspense.
Here is some example code for reference:
// components/ComponentA/index.js
import React from "react";
const style = {
display: "grid",
placeItems: "center"
};
export default function ComponentA() {
return <div style={style}>I am componentA</div>;
}
// components/ComponentB/index.js
import React from "react";
const style = {
display: "grid",
placeItems: "center"
};
export default function ComponentB() {
return <div style={style}>I am componentB</div>;
}
// App.js
import React from "react";
import "./styles.css";
const ComponentA = React.lazy(() => import("./components/ComponentA"));
const ComponentB = React.lazy(() => import("./components/ComponentB"));
export default function App() {
const [state, toggleState] = React.useReducer((state) => !state, false);
return (
<div className="App">
<button onClick={toggleState}>click to show</button>
<React.Suspense fallback={<div>Loading...</div>}>
{state ? <ComponentA /> : <ComponentB />}
</React.Suspense>
</div>
);
}
When running this code, I've noticed several things right away.
The first thing I noticed is that when the app loads for the first time, there is a split moment where you can see loading... before I am componentB is rendered.
The second thing I noticed is that if you click the button, you can, again, for a split moment see loading... before I am componentA is rendered on the screen.
I assume this is the expected result with dynamic imports.
The final thing that I noticed is that continuing to toggle the button, I never see loading... again. I assume that this is because the components have already been imported and used by the client. Which is, I also assume, expected behavior.
My three questions here are,
am I using React's dynamic imports correctly (or should I use import elsewhere) and,
when should React.Suspense and dynamic imports be used
If the components are relatively simple, do I need to even consider lazy loading components?
Sandbox for reference
For the First question -
Yes, you are using the lazy() function to import components Correctly.
For the second question -
From the React docs,
Suspense lets components “wait” for something before rendering.
It is used to improve your site's performance with regards to its First Contentful Paint and Loading times on slow or bad networks, as well as for asynchronous tasks like data fetching where your component depends on the data returned so you show a Loading.. message to the user as a Fallback.
For example, a component which I have written and used-
import React, { Component, lazy, Suspense } from "react";
import { Route, Switch } from "react-router-dom";
import Error404 from "./Error404";
import LoadingSpinner from "./LoadingSpinner";
const Dashboard = lazy(() => import("./Dashboard"));
const Employees = lazy(() => import("./EmployeesPage"));
class Container extends Component {
//handles paths '/' and '/employees' along with any unknown paths
render() {
return (
<div id="container">
<div id="background">
</div>
<div id="content">
<Switch>
<Route exact path="/">
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
</Route>
<Route path="/employees">
<Suspense fallback={<LoadingSpinner />}>
<Employees />
</Suspense>
</Route>
<Route component={Error404} />
</Switch>
</div>
</div>
);
}
}
export default Container;
Here, I am lazy loading my two components Dashboard and Employees which in themselves contain many components and are relatively complex.
Lazy loading them prevents the connection to be clogged while loading the site for the first time.
These components are now loaded only when the user navigates to the specified URL by some NavLinks.
Since it's safe to assume that the user will spend some time in the Dashboard before going to the Employees page(at least in my app), this approach works fine.
For the third question -
If you have components that you are sure will be needed when the site loads, it's better to not lazy load them to keep the User Experience good.
Small components (by their bundle size) don't need lazy loading, only when you think that many components are gonna be used only rarely, then you can lazy load them in one bundle.
Lastly -
You can open up the Network tab on the developer tools of a browser and see the bundles loading in as the user requests for them (React handles that in lazy()). The Bundles are labelled like this- Network Tab screenshot
Hope you find the answer useful and any recommendations for the post are appreciated✌

What is the right way to elevate props in React?

I apologize if my phrasing is wrong in the title. I've recently gotten cookies going in my app. My Topnav component needs access to them, but I'm unsure how to get them there.
App.js -
import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Landing from './pages/Landing.js';
import LoginPage from './pages/LoginPage.js';
import Topnav from './pages/components/global/Topnav.js';
import './Global.css';
const App = props => (
<div>
<Topnav />
<Switch>
<Route exact path='/' component={Landing} />
<Route exact path='/login' component={LoginPage} />
</Switch>
</div>
);
export default App;
My login component grabs the cookie from express (fetch) and then does a <Redirect to='/' />
This loads up my Landing page, where I'm able to grab the cookie, but how do I get the cookie to the Topnav? I saw an answer to something like this on stack where it seems like App.js grabs the cookie and passes it as a props to the components, but I don't see how it could if it never refreshes. I've thought about forcing an entire window refresh (which does work for Topnav when I do a refresh manually), but I've also seen answers here that say don't do that.
Use Context
You need to use the new context hook from react.
Create a context
This is a context that you can access around your app.
const MyContext = React.createContext(defaultValue);
Make a provider
Wrap the provider around your main app
<MyContext.Provider value={/* some value */}>
Access the context at the point at which you get the cookies
Use this in both your login and top nav to use the value from the context
const value = useContext(MyContext);
There are multiple ways to approach this.
Probably a beginner friendly one.
When your login Component does the login successfully you need to signal to the App Component about it probably using a onLoginSuccessful which can then read the cookie and do a setState with it and use this component state value in the props to your Topnav and Landing Component

ReactJS - Accessing data on Different Pages

I am trying to access data gathered from a user on one page and use it on another page. I have tried following these articles:
https://travishorn.com/passing-data-between-classes-components-in-react-4f8fea489f80
https://medium.com/#ruthmpardee/passing-data-between-react-components-103ad82ebd17
https://codeburst.io/react-js-pass-data-from-components-8965d7892ca2
I have not been able to get it to work. this.props.{variableName}keeps returning as undefined. My code is as follows.
The following is the Home Page:
import React, { Component } from 'react';
import {Button} from 'reactstrap';
import { browserHistory } from 'react-router';
class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
working: "",
};
}
WorkReqNav(){
this.setState=({working: "WORKING"});
browserHistory.push("/WorkReq");
}
render() {
return (
<div>
<Button size="lg" onClick={this.WorkReqNav.bind(this)} type='button'>HIT IT!</Button>
</div>
);
}
}
export default HomeScreen;
The following is the workReq screen:
import React, { Component } from 'react';
import {Button} from 'reactstrap';
class WorkReq extends Component {
constructor(props) {
super(props);
}
workCheck(){
var working = this.props.working;
alert(working);
}
render() {
return (
<div>
<Button size="lg" onClick={this.workCheck.bind(this)} type='button'>HIT IT!</Button>
</div>
);
}
}
export default WorkReq;
If you need anything more, please let me know. i am really new to React and this is my first time attempting anything like this.
welcome to the React world. I bet you'll love it when you gradually get familiar with cool stuff that you can do with React. Just be patient and keep practicing.
So the first suggestion I would make is that, like any other javascript environment, React also evolves very quickly. So although basic principles are the same, when you follow a new article on one hand, on the other hand you can check if the libraries or methodologies that are demonstrated are up to date.
Fasten your belts and let's do a quick review based on your question and libraries that I see you used in your example.
In terms of router, I see that you directly export things from react-router
When we check the npm page (https://www.npmjs.com/package/react-router) of react-router they make the following suggestion
If you are writing an application that will run in the browser, you
should instead install react-router-dom
Which is the following package https://www.npmjs.com/package/react-router-dom
You can get more details and find more tutorials in order to improve your skills by checking their official page https://reacttraining.com/react-router/web/guides/philosophy
Let's take a look at the code snippet sasha romanov provided that's based on react-router-dom syntax
with react-router-dom when you define a route with following syntax
<Route path="/" component={HomePage} exact />
react-router-dom automatically passes match, location, and history props to HomePage component. So when you console.log() these props, you should be able to display somethings on your console. And once you have access to history props, instead of browserHistory, you can use this.props.history.push("/some-route") for redirections.
Let's take a look at the part related to withRouter. In the example above, we could use history because HomePage component was passed directly to the Router component that we extract from react-router-dom. However, in real life, there might be cases in which you want to use history props in a component that's not passed to the Router but let's say just a reusable button component. For these cases, react-router-dom provides a Higher Order Component called withRouter
A Higher Order Component is (from React's official documentation)
https://reactjs.org/docs/higher-order-components.html
Concretely, a higher-order component is a function that takes a
component and returns a new component.
So basically, whenever you wrap any component with withRouter such as export default withRouter(MyWrappedReusableComponent), in your reusable component, you will have access to the props history, location, pathname
That said, my first impression regarding to your problem does not seem to be related to router logic but rather exchanging data between components.
In your original question, you mentioned that
I am trying to access data gathered from a user on one page and use it on another page
There are a couple of cases/ways to approach this issue
1) If these two components are completely irrelevant, you can use state management system such as Redux, mobx or you can use React's context API https://reactjs.org/docs/context.html. HOWEVER, since you are new to React, I would suggest not tackle with these right know till you are comfortable with the basic flow. Because at some point trying to implement a flow with a lot of libraries etc. is quite overwhelming. Believe me, I tried when I was also new to React and I was really close to break my computer after opening my 100th browser tab to look for another method from another library
2) You can implement a simple parent-child relationship to pass data between components. Let me explain what I mean by using references from your code snippet.
I believe you want to update working which is a state in your HomeScreen and you want to pass and use this updated value in your WorkReq component.
If we ignore all the routing logic and decide to go without routes, what you need to do is the following
import React, { Component } from 'react';
import {Button} from 'reactstrap';
import { browserHistory } from 'react-router';
import WorkReqComponent from 'path/to/WorkReqDirectory';
class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
working: "WORKING",
};
}
render() {
return (
<div>
<WorkReqComponent working={this.state.working} />
</div>
);
}
}
By this way, when you log this.props.working; in your WorkReqComponent you should be able to display the data that you passed. You can refer to this as passing data from parent to child.
I checked the articles you listed. They also seem to explain data transfer between parent to child, child to parent or between siblings.
In your case, what you really need to implement can be categorized as between siblings
I prepared a sample for you with react-router-dom to demonstrate one possible structure which might yield your expected outcome.
https://codesandbox.io/s/ojp2y0xxo6
In this example, the state is defined inside of the parent component called App. Also state update logic is also defined inside of the parent component. HomeScreen and WorkReq components are the children of App thus they are siblings. So, in order to transfer data between siblings, one of them was given the task of updating parent's state via passing state update logic to this component. The other one has the task of displaying parent's state's value.
At this point, since you are new and in order not to overwhelm yourself, you can experiment with parent-child-sibling data transfer topic. Once you are getting comfortable with the implementation and the logic, you can gradually start taking a look at React's context api and Redux/mobx.
Let me know if you have any questions regarding to the sample I provided
You can use react-router-dom lib and from seeing your code i think in parent component (app.js) you defined route for each child component you'd like to access
like this example here:
import { BrowserRouter, Route, Switch } from 'react-router-dom';
<BrowserRouter>
<div>
<Switch>
<Route path="/" component={HomePage} exact />
<Route path="/homescreen" component={HomeScreen} />
<Route path="/workreq" render={(props) => <WorkReq {...props} />} /> // here you can pass the props by calling render
<Route component={NoMatch} />
</Switch>
</div>
</BrowserRouter>
and then if you want to change route you can just call this.props.history.push('/workreq')
and if you didn't include route for the component in <BrowserRouter />
in the component that it's not included you can import withRouter and export like this withRouter(HomeScreen) and now you can access router props
if this isn't the answer you are looking please inform me to update my answer, i hope this can help

React-Router Redirect not working at all, using React-Redux-Electron

I am trying to conditionally redirect to other pages within a modal using from react-router.
I have implemented withRouter at the bottom of the relevant components as well as in the connect function. I am currently not using the Reducer because I have a switch in a root modal component which catches a type and then renders a component. Below is a snippet from the switch.
case type.componentName
return <Redirect to='/component' />
break;
However, the new component is still not rendering. It is as if Redirect is not being registered at all. My route is below.
<App>
<Switch>
<Route path="/" component={HomePage} />
<Route component={upperComponent}>
<Route path="/modal" component={rootComponent}>
<Route path="/component" component={component} />
</Route>
</Route>
</Switch>
</App>
I was originally rendering pages by modifying the state through a boolean and based upon it, a different component would be rendered. This worked just fine but I would rather have some history from using React-Router when I render new pages. Is there something fundamentally wrong about how I am trying to call Redirect or should I use an entirely different strategy all together?
The code below was requested in a comment. This is in my container component and I have one per component.
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import rootComponent from '../components/rooComponent';
import { withRouter } from 'react-router-dom';
import * as rootComponentlActions from '../actions/rootComponent';
function mapStateToProps(state) {
return {
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(rootComponentActions, dispatch);
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(rootComponent));
I have also tried adding export default withRouter(component); as well in my child component to the root as well as in the root to test it out based upon some examples that I have read in the past. As far as I can tell, it made not difference, good or bad.

React-router v4 this.props.history.push(...) not working

I'm trying to route programatically using this.props.history.push(..) but it doesn't seem to work.
Here's the router:
import {
BrowserRouter as Router,
Route
} from 'react-router-dom';
<Router>
<Route path="/customers/" exact component={CustomersList} />
<Route path="/customers/:id" exact component="{Customer} />
</Router>
In CustomerList, a list of customers is rendered. Clicking on a customer (li) should make the application route to Customer:
import { withRouter } from 'react-router'
class Customers extends Component {
static propTypes = {
history: PropTypes.object.isRequired
}
handleCustomerClick(customer) {
this.props.history.push(`/customers/${customer.id}`);
}
render() {
return(
<ul>
{ this.props.customers.map((c) =>
<li onClick={() => this.handleCustomerClick(c)} key={c.id}>
{c.name}
</li>
</ul>
)
}
}
//connect to redux to get customers
CustomersList = withRouter(CustomersList);
export default CustomersList;
The code is partial but illustrates perfectly the situation.
What happens is that the browser's address bar changes accordingly to history.push(..), but the view does not update, Customer component is not rendered and CustomersList is still there. Any ideas?
So I came to this question hoping for an answer but to no avail. I have used
const { history } = this.props;
history.push("/thePath")
In the same project and it worked as expected.
Upon further experimentation and some comparing and contrasting, I realized that this code will not run if it is called within the nested component. Therefore only the rendered page component can call this function for it to work properly.
Find Working Sandbox here
history: v4.7.2
react: v16.0.0
react-dom: v16.0.0
react-router-dom:
v4.2.2
It seems things have changed around a bit in the latest version of react router. You can now access history via the context. this.context.history.push('/path')
Also see the replies to the this github issue: https://github.com/ReactTraining/react-router/issues/4059
You can try to load the child component with history. to do so, pass 'history' through props. Something like that:
return (
<div>
<Login history={this.props.history} />
<br/>
<Register/>
</div>
)
For me (react-router v4, react v16) the problem was that I had the navigation component all right:
import { Link, withRouter } from 'react-router-dom'
class MainMenu extends Component {
render() {
return (
...
<NavLink to="/contact">Contact</NavLink>
...
);
}
}
export default withRouter(MainMenu);
Both using either
to="/contact"
or
OnClick={() => this.props.history.push('/contact')};
The behavior was still the same - the URL in browser changed but wrong components were rendered, the router was called with the same old URL.
The culprit was in the router definition. I had to move the MainMenu component as a child of the Router component!
// wrong placement of the component that calls the router
<MainMenu history={this.props.history} />
<Router>
<div>
// this is the right place for the component!
<MainMenu history={this.props.history} />
<Route path="/" exact component={MainPage} />
<Route path="/contact/" component={MainPage} />
</div>
</Router>
You can get access to the history object's properties and the closest 's match via the withRouter higher-order component. withRouter will pass updated match, location, and history props to the wrapped component whenever it renders.
import React, { Component } from 'react'
import { withRouter } from 'react-router';
// you can also import "withRouter" from 'react-router-dom';
class Example extends Component {
render() {
const { match, location, history } = this.props
return (
<div>
<div>You are now at {location.pathname}</div>
<button onClick={() => history.push('/')}>{'Home'}</button>
</div>
)
}
}
export default withRouter(Example)
Seems like an old question but still relevant.
I think it is a blocked update issue.
The main problem is the new URL (route) is supposed to be rendered by the same component(Costumers) as you are currently in (current URL).
So solution is rather simple, make the window url as a prop, so react has a chance to detect the prop change (therefore the url change), and act accordingly.
A nice usecase described in the official react blog called Recommendation: Fully uncontrolled component with a key.
So the solution is to change from
render() {
return(
<ul>
to
render() {
return(
<ul key={this.props.location.pathname}>
So whenever the location changed by react-router, the component got scrapped (by react) and a new one gets initiated with the right values (by react).
Oh, and pass the location as prop to the component(Costumers) where the redirect will happen if it is not passed already.
Hope it helps someone.
I had similar symptoms, but my problem was that I was nesting BrowserRouter
Do not nest BrowserRouter, because the history object will refer to the nearest BrowserRouter parent. So when you do a history.push(targeturl) and that targeturl it's not in that particular BrowserRouter it won't match any of it's route, so it will not load any sub-component.
Solution
Nest the Switch without wrapping it with a BrowserRouter
Example
Let's consider this App.js file
<BrowserRouter>
<Switch>
<Route exact path="/nestedrouter" component={NestedRouter} />
<Route exact path="/target" component={Target} />
</Switch>
</BrowserRouter>
Instead of doing this in the NestedRouter.js file
<BrowserRouter>
<Switch>
<Route exact path="/nestedrouter/" component={NestedRouter} />
<Route exact path="/nestedrouter/subroute" component={SubRoute} />
</Switch>
</BrowserRouter>
Simply remove the BrowserRouter from NestedRouter.js file
<Switch>
<Route exact path="/nestedrouter/" component={NestedRouter} />
<Route exact path="/nestedrouter/subroute" component={SubRoute} />
</Switch>
Let's consider this scenario. You have App.jsx as the root file for you ReactJS SPA. In it your render() looks similar to this:
<Switch>
<Route path="/comp" component={MyComponent} />
</Switch>
then, you should be able to use this.props.history inside MyComponent without a problem. Let's say you are rendering MySecondComponent inside MyComponent, in that case you need to call it in such manner:
<MySecondComponent {...props} />
which will pass the props from MyComponent down to MySecondComponent, thus making this.props.history available in MySecondComponent
You need to export the Customers Component not the CustomerList.
CustomersList = withRouter(Customers);
export default CustomersList;
I see that you are using a class component but in case you decide to switch to functional component or encountered the same issue with a functional component in your application, you can fix this issue by using the "useHistory" hook API by react-router-dom.
Example of usage:
import { useHistory } from "react-router-dom";
const Customers = ({customer}) => {
let history = useHistory();
const handleCustomerClick = (customer) => {
history.push(`/customers/${customer.id}`);
}
return (
//some JSX here
);
};
You may find the official documentation here: https://reactrouter.com/web/api/Hooks/usehistory
Beginner's mistake when working with routing is the importance of using withRouter directly with the component and not put any other high order component in between (or at least one that doest not know to push the props.history to its children:
Wrong: export default withRouter(withErrorHandler(Foo));
Correct: export default withErrorHandler(withRouter(Foo));
`const navigate=useNavigate();
navigate(/customers/${customer.id}); `
Don't use with Router.
handleSubmit(e){
e.preventDefault();
this.props.form.validateFieldsAndScroll((err,values)=>{
if(!err){
this.setState({
visible:false
});
this.props.form.resetFields();
console.log(values.username);
const path = '/list/';
this.props.history.push(path);
}
})
}
It works well.
You need to bind handleCustomerClick:
class Customers extends Component {
constructor() {
super();
this.handleCustomerClick = this.handleCustomerClick(this)
}
this.props.history.push(`/customers/${customer.id}`, null);

Categories

Resources