HOC as an App Wrapper for React Redux - reactjs

I wanted to have an app HOC that wraps each component view.
This HOC authenticates user and sets Google Analytics tracking.
I'm upgrading to Router 4, and having an issue with making it work.
It's giving me the following error -
TypeError: (0 , _AppWrapper2.default) is not a function
Which is likely related to how I am creating the HOC.
Any ideas?
routes.js
export default (
<Switch>
<Route exact path="/" component={AppWrapper(Home)} />
<Route exact path="/channels" component={AppWrapper(Channels)} />
</Switch>
);
const AppWrapper = (WrappedComponent) => {
return class AppWrapperComponent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const page = this.props.location.pathname;
this.trackPage(page);
}
componentWillReceiveProps(nextProps) {
const currentPage = this.props.location.pathname;
const nextPage = nextProps.location.pathname;
if (currentPage !== nextPage) {
this.trackPage(nextPage);
}
}
trackPage = page => {
GoogleAnalytics.set({
page,
...options,
});
GoogleAnalytics.pageview(page);
};
render() {
return (
<div>
{this.state.isMounted && !window.devToolsExtension && process.env.NODE_ENV === 'development' && <DevTools />}
<WrappedComponent {...this.props.chidren} />
</div>
);
}
}

Looks like you're not exporting AppWrapper. If you import it with import AppWrapper from .., add this line at the end of AppWrapper.js:
export default AppWrapper;
or replace the const declaration with
export default (WrappedComponent) => { ..
If you import it with import {AppWrapper} from .., you can insert an export before the const:
export const AppWrapper = (WrappedComponent) => {

Related

React Router Dom - v6 - useBlocker

As https://github.com/remix-run/react-router/issues/8139 is finished and we got useBlocker in v6, did anyone got it to work?
This is what I got so far and pretty much I'm stuck with error I quite don't understand
in App.js I have my BrowserRouter and everything is wrapped inside
Also I used example from implementer's gists (I copy pasted)
import * as React from "react";
import { useBeforeUnload, unstable_useBlocker as useBlocker } from "react-router-dom";
function usePrompt(message, { beforeUnload } = {}) {
let blocker = useBlocker(
React.useCallback(
() => (typeof message === "string" ? !window.confirm(message) : false),
[message]
)
);
let prevState = React.useRef(blocker.state);
React.useEffect(() => {
if (blocker.state === "blocked") {
blocker.reset();
}
prevState.current = blocker.state;
}, [blocker]);
useBeforeUnload(
React.useCallback(
(event) => {
if (beforeUnload && typeof message === "string") {
event.preventDefault();
event.returnValue = message;
}
},
[message, beforeUnload]
),
{ capture: true }
);
}
function Prompt({ when, message, ...props }) {
usePrompt(when ? message : false, props);
return null;
}
And then within my component I called Prompt like this
const MyComponent = (props) => {
const [showPrompt, setShowPrompt] = useState(false)
...
return (
...
<Prompt when={showPrompt}
message="Unsaved changes detected, continue?"
beforeUnload={true}
/>
)
}
And on page load of MyComponent I keep getting error
Error: useBlocker must be used within a data router. See
https://reactrouter.com/routers/picking-a-router.
at invariant (history.ts:308:1)
at useDataRouterContext (hooks.tsx:523:1)
at useBlocker (hooks.tsx:723:1)
at usePrompt (routerCustomPrompt.js:8:1)
at Prompt (routerCustomPrompt.js:37:1)
Did anyone got useBlocker in new version to work?
The error message is rather clear. In order to use the useBlocker hook it must be used within a component rendered by a Data router. See Picking a Router.
Example:
const MyComponent = (props) => {
const [showPrompt, setShowPrompt] = useState(false);
...
return (
...
<Prompt
when={showPrompt}
message="Unsaved changes detected, continue?"
beforeUnload={true}
/>
);
}
import {
createBrowserRouter,
createRoutesFromElements,
Route,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
{/* ... etc. */}
<Route path="myComponent" element={<MyComponent />} />
{/* ... etc. */}
</Route>
)
);
const App = () => <RouterProvider router={router} />;

In React, is there an elegant way of using the id in a RESTful edit url and loading the corresponding object into the initial state of my component?

I'm building a React 16.13 application. I have a search component, src/components/Search.jsx, that constructs search results and then builds a URL to edit those results ...
renderSearchResults = () => {
const { searchResults } = this.state;
if (searchResults && searchResults.length) {
return (
<div>
<div>Results</div>
<ListGroup variant="flush">
{searchResults.map((item) => (
<ListGroupItem key={item.id} value={item.name}>
{item.name}
<span className="float-right">
<Link to={"/edit/"+item.id}>
<PencilSquare color="royalblue" size={26} />
</Link>
</span>
</ListGroupItem>
))}
</ListGroup>
</div>
);
}
};
render() {
return (
<div className="searchForm">
<input
type="text"
placeholder="Search"
value={this.state.searchTerm}
onChange={this.handleChange}
/>
{this.renderSearchResults()}
</div>
);
}
Is there a more elegant way to load/pass the object I want to edit? Below I'm deconstructing the URL and launching an AJAX call but what I'm doing seems kind of sloppy. I'm familiar with Angular resolvers and that seems a cleaner way of decoupling the logic of parsing the URL and finding the appropriate objects but the below is all I could come up with ...
src/components/Edit.jsx
import React, { Component } from "react";
import FormContainer from "../containers/FormContainer";
export default class Edit extends Component {
render() {
return <FormContainer />;
}
}
src/containers/FormContainer.jsx
class FormContainer extends Component {
...
componentDidMount() {
let initialCountries = [];
let initialProvinces = [];
let coopTypes = [];
// Load form object, if present in URL
const url = window.location.href;
const id = url.split("/").pop();
fetch(FormContainer.REACT_APP_PROXY + "/coops/" + id)
.then((response) => {
return response.json();
})
.then((data) => {
const coop = data;
coop.addresses.map(address => {
address.country = FormContainer.DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
this.setState({
newCoop: coop,
});
});
You aren't posting all the relevant code but I know what you are trying to accomplish (correct me if I'm wrong). You want to use the id from the url parameters to fetch data. I think you are using react-router. You can use this example to refactor your code:
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
useParams
} from "react-router-dom";
const REACT_APP_PROXY = "api";
const DEFAULT_COUNTRY_CODE = "20";
// You can use functional components and react hooks in React 16.13 to do everything
// No need for class components any more
function FormContainer() {
// useState hook to handle state in functional components
const [newCoop, setNewCoop] = useState({});
// useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
const { id } = useParams();
// This will be called whenever one of the values in the dependencies array (second argument) changes
// but you can pass an empty array to make it run once
useEffect(() => {
fetch(REACT_APP_PROXY + "/coops/" + id)
.then(response => {
return response.json();
})
.then(data => {
const coop = data;
coop.addresses.map(address => {
address.country = DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
setNewCoop(coop);
});
// use an empty array as the second argument to run this effect on the first render only
// it will give a similar effect to componentDidMount
}, []);
return <div>Editing {id}</div>;
}
const Edit = () => <FormContainer />;
function App() {
return (
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/edit/:id">
<Edit />
</Route>
</Switch>
</Router>
);
}

Component in Component, React redux

Is it possible to call component in component (Like inception)
Example
Content.jsx
class Content extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.dispatch(fetchNav(this.props.match.params.tab));
}
componentDidUpdate(prevProps) {
if(this.props.match.params.tab != prevProps.match.params.tab) {
this.props.dispatch(fetchNav(this.props.match.params.tab));
}
}
render() {
const {nav, match} = this.props;
let redirect = null;
return (
<div>
<ul>
{nav.some((item, key) => {
if (this.props.location.pathname != (match.url + item.path)) {
redirect = <Redirect to={(match.url + item.path)} />;
}
return true;
})}
{redirect}
{nav.map((item, key) => {
return <li key={key}>
<Link to={match.url + item.path}>{item.name}</Link>
</li>;
})}
<Switch>
{nav.map((item, key) => {
return <Route key={key} path={`${match.url}/list/:tab`} component={Content} />;
})}
</Switch>
</ul>
</div>
);
}
}
const mapStateToProps = (state, props) => {
const {fetchNav} = state;
const {
lastUpdated,
isFetching,
nav: nav
} = fetchNav[props.match.params.tab] || {
isFetching: true,
nav: []
};
return {
nav,
isFetching,
lastUpdated
}
};
export default connect(mapStateToProps)(withStyles(appStyle)(Content));
Actually when i do this, if my route match and call same "Content" component, it says : "this.props.dispatch is not a function"
Do you think i need to create a ContentContainer that manage connect() and pass a method via props to manage change ?
Many thanks for you answers
Regards,
Thomas.
You are clearly not mapping dispatch to your props in your connect.
See the docs for redux' connect method:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
You can map the dispatch function to your props like like this:
const mapDispatchToProps = (dispatch) => ({ dispatch });
connect(mapStateToProps, mapDispatchToProps)...
Do you think i need to create a ContentContainer that manage connect() and pass a method via props to manage change ?
You don't need a container. The separation of code between a container and a (view-) component is just a pattern and not required.
As a sidenote: I would recommend to use compose to combine your HOCs, see this or this.

React lazy does not cause splitting bundle in chunks

As in the title, I'm trying to use React.lazy feature which works in my my other project. But not in this one, I don't know what I'm missing here. All works just fine, no errors, no warnings. But for some reason I don't see my bundle split in chunks.
Here's my implementation:
import React, { Component, Suspense } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { getApps } from '../../actions/apps';
import './_apps.scss';
const AppItemComponent = React.lazy(() => import('../AppItem'));
class Apps extends Component {
componentDidMount() {
const { getApps } = this.props;
getApps(3);
}
renderAppItem = () => {
const { apps } = this.props;
return apps && apps.map((item, i) => {
return (
<Suspense fallback={<div>loading...</div>} key={i}>
<AppItemComponent
index={i + 1}
item={item}
/>
</Suspense>
);
});
};
render() {
const { apps } = this.props;
return (
<div className="apps__section">
<div className="apps__container">
<div className="apps__header-bar">
<h3 className="apps__header-bar--title">Apps</h3>
<Link className="apps__header-bar--see-all link" to="/apps">{`see all (${apps.length})`}</Link>
</div>
{this.renderAppItem()}
</div>
</div>
);
}
}
const mapStateToProps = ({ apps }) => {
return { apps };
};
const mapDispatchToProps = dispatch => {
return {
getApps: quantity => dispatch(getApps(quantity)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Apps);
I'm doing this in react-create-app app and in react v16.6, react-dom v16.6.
What am I missing here?
I also have the same problem, then I have resolved this case without using Suspense and lazy(try code below), and I can see chunks file. However, after using this way, I try changing my code again with Suspense and lazy. It works!!! and I don't know why it does. Hope that it works for someone find solution for this case.
1 - create file asyncComponent
import React, { Component } from "react";
const asyncComponent = (importComponent) => {
return class extends Component {
state = {
component: null,
};
componentDidMount() {
importComponent().then((cmp) => {
this.setState({ component: cmp.default });
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : null;
}
};
};
export default asyncComponent;
2 - and in App.js, example:
const AuthLazy = asyncComponent(() => import("./containers/Auth/Auth"));
//Route
<Route path="/auth" component={AuthLazy} />
Check that your component is not imported somewhere with regular import: import SomeComponent from './path_to_component/SomeComponent';
Check that component is not re-exported somewhere. (For example in index.js (index.ts) file: export * from './SomeComponent') If so, just remove re-export of this component.
Check that your export your component as default or use code like this:
const SomeComponent = React.lazy(() => import('./path_to_component/SomeComponent').then((module) => ({ default: module.SomeComponent })));

this.props.history.push not re-rendering react component

In my component I use this.props.history.push(pathname:.. search:..) to rerender the component and fetch new data form a third party service. When I first call the page it renders. But when I call history push inside the component the URL updates correctly BUT the component doesn't rerender. I read a lot but couldn't get it working. Any ideas?
I'm using react router v4
//index.js
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login}/>
<Route path="/" component={Main}/>
</Switch>
</BrowserRouter>
</Provider>
//Main.js
//PropsRoute is used to push props to logs component so I can use them when fetching new data
const PropsRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={props => <Component {...props} />}/>
);
};
class Main extends Component {
render() {
return (
<div className="app">
<NavigationBar/>
<div className="app-body">
<SideBar/>
<Switch>
<PropsRoute path="/logs" component={Log}/> //this component is not rerendering
<Route path="/reports" component={Reports}/>
<Route path="/gen" component={Dashboard}/>
<Redirect from="/" to="/gen"/>
</Switch>
</div>
</div>
)
}
}
export default Main;
//inside 'Log' component I call
import React, {Component} from 'react';
import {getSystemLogs} from "../api";
import {Link} from 'react-router-dom';
import _ from "lodash";
import queryString from 'query-string';
let _isMounted;
class Log extends Component {
constructor(props) {
super(props);
//check if query params are defined. If not re render component with query params
let queryParams = queryString.parse(props.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
this.props.history.push({
pathname: '/logs',
search: `?page=1&pageSize=25&type=3&application=fdce4427fc9b49e0bbde1f9dc090cfb9`
});
}
this.state = {
logs: {},
pageCount: 0,
application: [
{
name: 'internal',
id: '...'
}
],
types: [
{
name: 'Info',
id: 3
}
],
paginationPage: queryParams.page - 1,
request: {
page: queryParams.page === undefined ? 1 : queryParams.page,
type: queryParams.type === undefined ? 3 : queryParams.type,
pageSize: queryParams.pageSize === undefined ? 25 : queryParams.pageSize,
application: queryParams.application === undefined ? 'fdce4427fc9b49e0bbde1f9dc090cfb9' : queryParams.application
}
};
this.onInputChange = this.onInputChange.bind(this);
}
componentDidMount() {
_isMounted = true;
this.getLogs(this.state.request);
}
componentWillUnmount() {
_isMounted = false;
}
getLogs(request) {
getSystemLogs(request)
.then((response) => {
if (_isMounted) {
this.setState({
logs: response.data.Data,
pageCount: (response.data.TotalCount / this.state.request.pageSize)
});
}
});
}
applyFilter = () => {
//reset page to 1 when filter changes
console.log('apply filter');
this.setState({
request: {
...this.state.request,
page: 1
}
}, () => {
this.props.history.push({
pathname: '/logs',
search: `?page=${this.state.request.page}&pageSize=${this.state.request.pageSize}&type=${this.state.request.type}&application=${this.state.request.application}`
});
});
};
onInputChange = () => (event) => {
const {request} = this.state; //create copy of current object
request[event.target.name] = event.target.value; //update object
this.setState({request}); //set object to new object
};
render() {
let logs = _.map(this.state.logs, log => {
return (
<div className="bg-white rounded shadow mb-2" key={log.id}>
...
</div>
);
});
return (
<main className="main">
...
</main>
);
}
}
export default Log;
Reactjs don't re-run the constructor method when just props or state change, he call the constructor when you first call your component.
You should use componentDidUpdate and do your fetch if your nextProps.location.pathname is different than your this.props.location.pathname (react-router location)
I had this same issue with a functional component and I solved it using the hook useEffect with the props.location as a dependency.
import React, { useEffect } from 'react';
const myComponent = () => {
useEffect(() => {
// fetch your data when the props.location changes
}, [props.location]);
}
This will call useEffect every time that props.location changes so you can fetch your data. It acts like a componentDidMountand componentDidUpdate.
what about create a container component/provider with getderivedstatefromprops lifecycle method, its more react-look:
class ContainerComp extends Component {
state = { needRerender: false };
static getderivedstatefromprops(nextProps, nextState) {
let queryParams = queryString.parse(nextProps.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
return { needRefresh: true };
} else {
return { needRefresh: false };
}
}
render() {
return (
<div>
{this.state.needRefresh ? <Redirect params={} /> : <Log />}
</div>
);
}
}

Resources