How can I use React component as an enhancer? - reactjs

I'm a newbie and React and I've got the following piece of code:
class MyCoolComponent extends React.Component {
render() {
return (
<QueryRenderer
environment={environment}
query={graphql`
query UserQuery {
viewer {
id
}
}
`}
variables={{}}
render={({error, props}) => {
if (error) {
return <div>Error!</div>;
}
if (!props) {
return <div>Loading...</div>;
}
return <div>User ID: {props.viewer.id}</div>;
}}
/>
);
}
}
I want to use this React component as an enhancer to a different React Component to pass the data (props.viewer.id) using compose(addDataEnhancer, ...) to the other High Order Component (such that it'll be able to use props.viewer.id). How can I do it?
The context: the idea is to use this MyCoolComponent as a replacement for the data source function here:
export default compose(
// data source
graphql(gql`query MyQuery1 { ... }`),
)(MyHOCComponent);
function MyHOCComponent({ data }) {
console.log(data);
}

I think this should work. compose will call first hoc with base component, than return value (wrapped component) is passed to next hoc (next compose argument), etc...
Base component (and every HOC listed after this HOC) will have prop viewerId
// withViewerId.js
import React from 'react'
import { QueryRenderer, graphql } from 'react-relay'
import environment from 'environment'
export default (BaseComponent) => (restProps) => (
<QueryRenderer
environment={environment}
query={graphql`
query UserQuery {
viewer {
id
}
}
`}
variables={{}}
render={({error, props}) => {
if (error) {
return <div>Error!</div>;
}
if (!props) {
return <div>Loading...</div>;
}
return <BaseComponent {...restProps} viewerId={props.viewer.id} />;
}}
/>
)
usage:
import withViewerId from 'withViewerId'
export default compose(withViewerId)(BaseComponent)

Related

React/TypeScript `Error: Maximum update depth exceeded.` when trying to redirect on timeout

I have a project that I'm trying to get to redirect from page 1 to 2 etc. dynamically. This has worked for me previously, but recently I'm getting this error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
After seeing this message this morning, and multiple SO pages saying NOT to call setState in render, I have moved my setTimeout call into componentDidMount.
So far I've tried
- calling a function that changes this.props.pageWillChange property, then in render I return a object based on that condition
- returning a object pending condition set in an inline if statement in render
- turning pageWillChange into a local prop, rather than one that is inherited by the class (I quite like this option, as the state of this will be the same for every new version of this component)
Many more things, but these felt like they would work. Anyone able to help?
import React, { Component } from "react"
import axios from "axios"
import { GridList, GridListTile } from "#material-ui/core"
import "../assets/scss/tile.scss"
import Request from "../config.json"
import DataTile from "./DiagnosticDataTile"
import { IDiagnosticResultData } from "../interfaces/IDiagnosticResultData"
import { Redirect } from "react-router"
interface IProps {
category: string
redirect: string
}
interface IPageState {
result: IDiagnosticResultData[]
pageWillChange: boolean
}
class Dashboard extends Component<IProps, IPageState> {
_isMounted = false
changeTimeInMinutes = 0.25
willRedirect: NodeJS.Timeout
constructor(props: Readonly<IProps>, state: IPageState) {
super(props)
this.state = state
console.log(window.location)
}
componentDidMount(): void {
this._isMounted = true
this.ChangePageAfter(this.changeTimeInMinutes)
axios
.get(Request.url)
.then(response => {
if (this._isMounted) {
this.setState({ result: response.data })
}
})
.catch(error => {
console.log(error)
})
}
componentWillUnmount(): void {
this._isMounted = false
clearTimeout(this.willRedirect)
}
ChangePageAfter(minutes: number): void {
setTimeout(() => {
this.setState({ pageWillChange: true })
}, minutes * 60000)
}
render() {
var data = this.state.result
//this waits for the state to be loaded
if (!data) {
return null
}
data = data.filter(x => x.categories.includes(this.props.category))
return (
<GridList
cols={this.NoOfColumns(data)}
cellHeight={this.GetCellHeight(data)}
className="tileList"
>
{this.state.pageWillChange ? <Redirect to={this.props.redirect} /> : null}
{data.map((tileObj, i) => (
<GridListTile
key={i}
className="tile"
>
<DataTile data={tileObj} />
</GridListTile>
))}
</GridList>
)
}
}
export default Dashboard
(very new with React and TypeScript, and my first SO post woo!)
Try the code below, also couple of points:
No need for _isMounted field. Code in 'componentDidMount' always runs after it's mounted.
No need to set state in constructor. Actually there is no need for constructor anymore.
I can't see much point of clearTimeout in componentWillUnmount mount. It's never asigned to timeout.
About routing. U can use 'withRouter' high order function to change route programmatically in changePageAfter method.
Hope this helps!
import axios from "axios"
import { GridList, GridListTile } from "#material-ui/core"
import "../assets/scss/tile.scss"
import Request from "../config.json"
import DataTile from "./DiagnosticDataTile"
import { IDiagnosticResultData } from "../interfaces/IDiagnosticResultData"
import { Redirect, RouteComponentProp } from "react-router"
interface PropsPassed {
category: string
redirect: string
}
type Props = PropsPassed & RouteComponentProp
interface IPageState {
result: IDiagnosticResultData[]
pageWillChange: boolean
}
class Dashboard extends Component<Props, IPageState> {
changeTimeInMinutes = 0.25
willRedirect: NodeJS.Timeout
componentDidMount(): void {
this.ChangePageAfter(this.changeTimeInMinutes)
axios
.get(Request.url)
.then(response => {
this.setState({ result: response.data })
})
.catch(error => {
console.log(error)
})
}
changePageAfter(minutes: number): void {
setTimeout(() => {
this.props.history.push({
pathname: '/somepage',
});
}, minutes * 60000)
}
render() {
var data = this.state.result
//this waits for the state to be loaded
if (!data) {
return null
}
data = data.filter(x => x.categories.includes(this.props.category))
return (
<GridList
cols={this.NoOfColumns(data)}
cellHeight={this.GetCellHeight(data)}
className="tileList"
>
{data.map((tileObj, i) => (
<GridListTile
key={i}
className="tile"
>
<DataTile data={tileObj} />
</GridListTile>
))}
</GridList>
)
}
}
export default withRouter(Dashboard)

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 })));

trying to pass my arrays (props) into my publish function as selector

import { Mongo } from 'meteor/mongo';
import { Meteor } from 'meteor/meteor';
import React, {Component} from 'react';
import {check} from 'meteor/check';
export const Adressen = new Mongo.Collection('Phonebook');
if (Meteor.isServer) {
Meteor.publish('ArrayToExport', function(branches) {
check(branches, [Match.Any]);
if(branches.length > 10){
return this.ready()
};
return Adressen.find(
{branche: {$in: branches}}, {fields: {firmenname:1, plz:1}}
);
});
}
.
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import {Adressen} from "../api/MongoDB";
class ExportArray extends Component{
constructor(props){
super(props);
this.state = {
branches: this.props.filteredBranches
};
}
render(){
return(
<div>
<button onClick={this.exportArrays}></button>+
</div>
);
}
}
export default withTracker( (branches) => {
Meteor.subscribe('ArrayToExport', branches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
this.props.filteredBranche is a pure array,generated through controlled input field. this.props.filteredBranches changes as Input changes, in parent Component.
I thought I was sending my this.props.filteredBranches as an argument through withTracker function. But nothing is passed to the publish function.
if (Meteor.isServer) {
arrayExfct = function (array){
return {
find: {branche:{$in: array }},
fields: {firmenname:1, plz:1}
};
}
Meteor.publish('ArrayToExport', function (array) {
return Adressen.find(
arrayExfct(array).find, arrayExfct(array).fields);
});
}
.
export default withTracker( () => {
arrayExfct = function(array) {
return {
find: {branche: {$in: array}},
fields: {firmenname:1, plz:1}
}
}
var array = ['10555'];
Meteor.subscribe('ArrayToExport', array );
var arrayExfct = Adressen.find(arrayExfct(array).find, arrayExfct(array).fields);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
It would help if you also added an example of where you used this component and how you pass props to it, but I think I see your problem.
You expect the local state in your rendering component to get into the withTracker container, but that would be the other way around. When you make the withTracker container, you are really making another react component that renders your display component (ExportArray) and passes the data (ArrayToExport) down into it.
So, props go like this currently:
external render -> withTracker component -> ExportArray
What you need to do it to get the filteredBranches (which you pass from a parent component?) from the props argument in withTracker and pass that to the subscribtion,
class ExportArray extends Component{
exportArrays () {
const { ArrayToExport } = this.props;
}
render(){
const { ArrayToExport } = this.props;
return(
<div>
<button onClick={this.exportArrays}></button>+
</div>
);
}
}
export default withTracker(propsFromParent => {
const { filteredBranches } = propsFromParent;
Meteor.subscribe('ArrayToExport', filteredBranches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
Hi the issue is with the code below. The parameter called branches is the props so branches.branches is the array you passed in.
export default withTracker( (branches) => {
Meteor.subscribe('ArrayToExport', branches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
Try the following.
export default withTracker( ({branches}) => {
Meteor.subscribe('ArrayToExport', branches);
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
Notice all that changed was
(branches)
became
({branches})
I solved my problem with a combination of Session Variables and State.
//Client
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import {Adressen} from "../api/MongoDB";
import {Meteor} from 'meteor/meteor';
import { Session } from 'meteor/session';
class ExportArray extends Component{
constructor(){
super();
this.state = {
x: [],
y: []
};
this.exportArrays = this.exportArrays.bind(this);
}
exportArrays(e){
e.preventDefault();
this.setState({x: this.props.filteredBranches});
this.setState({y: this.props.filteredPostleitzahlen});
}
render(){
var selector = {branche: {$in: this.state.x},plz: {$in: this.state.y}};
Session.set('selector', selector);
return(
<div>
<button onClick={this.exportArrays}> Commit </button>
</div>
);
}
}
export default withTracker( () => {
const ArrayfürExport = Meteor.subscribe('ArrayToExport', Session.get('selector') );
return {
ArrayToExport: Adressen.find({}).fetch()
};
})(ExportArray);
//Server
Meteor.publish('ArrayToExport', function (selector) {
console.log('von mongodb', selector);
return Adressen.find(
selector
, {
fields: {firmenname:1, plz:1}
});
});
}

How to initialize state with api's data

I'm creating a React/Redux app that fetches data from an api (pokeapi.co). I fetch data using axios. When I display data on react components, it results in an error that data is undefined. After some digging, I find that my state at first returns initial state which is empty object then it returns the api data. but it dont display on react. I'm new to React so I'm guessing it has to do with the axios asynchronous functionality. How do you set state with api's initial data or wait on rendering the data till state has api's data?
Here is the Reducer
function pokemonReducer(state={}, action) {
switch (action.type) {
case pokemonsActions.GET_POKEMON_SUCCESS:
{
return {...state, data: action.payload.data}
}
default:
{
return state;
}
}
}
export default pokemonReducer
Here is the action
export const GET_POKEMON_SUCCESS = 'GET_POKEMON_SUCCESS'
export const GET_POKEMON_ERROR = 'GET_POKEMON_ERROR'
function getPokemonSuccess(response) {
return {
type: GET_POKEMON_SUCCESS,
payload: response
}
}
function getPokemonError(err) {
return {
type: GET_POKEMON_ERROR,
payload: err
}
}
export function getPokemon() {
return (disp,getState) =>
{
return pokeAPI.getPokeAPI()
.then((response) => { disp(getPokemonSuccess(response))})
.catch((err)=> disp(getPokemonError(err)))
}
}
Store
const loggerMiddleware = createLogger()
const middleWare= applyMiddleware(thunkMiddleware,loggerMiddleware);
const store = createStore(rootReducer,preloadedState,
compose(middleWare, typeof window === 'object' && typeof window.devToolsExtension !== 'undefined'
? window.devToolsExtension() : (f) => f
))
const preloadedState=store.dispatch(pokemonActions.getPokemon())
export default store
in React component
function mapStateToProps(state) {
return {
pokemons:state.pokemons
}
}
class PokemonAbility extends React.Component {
render(){
return (
<div>
<div className="header">
<h1>Fetch Poke Api with axios</h1>
</div>
<main>
<h3> Display pokemons abilities </h3>
<p>{this.props.pokemons.data.count}</p>
</main>
</div>
)
}
}
export default connect(
mapStateToProps
)(PokemonAbility)
Api data example
{
"count": 292,
"previous": null,
"results": [
{
"url": "https://pokeapi.co/api/v2/ability/1/",
"name": "stench"
},
{
"url": "https://pokeapi.co/api/v2/ability/2/",
"name": "drizzle"
},
{
"url": "https://pokeapi.co/api/v2/ability/3/",
"name": "speed-boost"
}
],
"next": "https://pokeapi.co/api/v2/ability/?limit=20&offset=20"
}
You're rendering your component before the data has loaded. There are many strategies for dealing with this. In no particular order, here are some examples:
1. Short circuit the render
You can short circuit the render by returning a loading message if the data isn't there:
function mapStateToProps(state) {
return {
pokemons:state.pokemons
}
}
class PokemonAbility extends React.Component {
render(){
if (!this.props.pokemons.data) {
return (
<div>Loading...</div>
);
}
return (
<div>
<div className="header">
<h1>Fetch Poke Api with axios</h1>
</div>
<main>
<h3> Display pokemons abilities </h3>
<p>{this.props.pokemons.data.count}</p>
</main>
</div>
);
}
}
export default connect(mapStateToProps)(PokemonAbility);
2. Lift the data check to a parent component
You can move the mapStateToProps into a higher component, or abstract out the view component, and only render the view when the data is ready:
function mapStateToProps(state) {
return {
pokemons:state.pokemons
}
}
class SomeHigherComponent extends React.Component {
render(){
return (
this.props.pokemons.data ?
<PokemonAbility pokemons={this.props.pokemons} /> :
<div>Loading...</div>
);
}
}
3. Higher order component data checking
You could wrap your components in a "higher order component" (a function that takes a component class and returns a component class) to check if that prop exists before rendering:
function EnsurePokemon(ChildComponent) {
return class PokemonEnsureWrapper extends React.Component {
render() {
return (
this.props.pokemons.data ?
<ChildComponent {...this.props} /> :
<div>Loading...</div>
);
}
}
}
Usage:
export default connect(
mapStateToProps
)(EnsurePokemon(PokemonAbility))
And you can wrap any child component in this EnsurePokemon HOC to make sure it doesn't render until the data has loaded.

The reducer does not transmit the initialState

The this.props does not have the loading, error key.
Why?
My code on :
https://github.com/jiexishede/react-redux-demo01
You can fork it and pull request.
Because you don't pass them in your mapStateToProps function
https://github.com/jiexishede/react-redux-demo01/blob/0c1407935cd6c461705d6ca37f3e33484afac327/src/views/Home.js#L8-L10
This should be something like:
#connect(state => {
return {
articleList: state.home.list.articleList,
loading: state.home.list.loading,
error: state.home.list.error,
};
You didn't set up your component to receive updates from your store. Your component won't know that the reducer has updated the state. Check out the code below:
import React, { Component } from 'react';
import * as Redux from 'react-redux'; // Import redux
import Preview from './Preview'
class PreviewList extends Component {
static propTypes = {
loading:React.PropTypes.bool, // 注意 bushi PropTypes.bool, 前面要价 React
error:React.PropTypes.bool,
articleList: React.PropTypes.arrayOf(React.PropTypes.object),
loadArticles: React.PropTypes.func
};
componentDidMount(){
this.props.loadArticles();
}
render(){
const { loading, error, articleList } = this.props;
if(error){
return <p className="message">)0ops, something is wrong. </p>
}
if(loading){
return <p className="message">Loading....</p>
}
// return this.props.articleList.map(item => (
// <Preview {...item} key={item.id}/>
// ))
return (
<div>
{articleList.map(item => {
return <Preview {...item} key={item.id} push={this.props.push} />
})}
</div>
);
}
}
// Connect your component to your store and
// receive updates from your previewList reducer:
export default Redux.connect(state => {
return {
loading: state.previewList.loading,
error: state.previewList.error,
articleList: state.previewList.articleList
};
})(PreviewList);
The problem is that you're not connecting your component to the redux store. You need to install the react-redux package then use it's connect function to connect your component to the store like the following:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Preview from './Preview';
import { loadArticles } from './PreviewListRedux';
class PreviewList extends Component {
static propTypes = {
loading:React.PropTypes.bool,
error:React.PropTypes.bool,
articleList: React.PropTypes.arrayOf(React.PropTypes.object),
loadArticles: React.PropTypes.func
};
componentDidMount(){
this.props.loadArticles();
}
render(){
if (!this.props.loading) {
return <div>Loading...</div>
}
const { loading, error, articleList } = this.props;
if(error){
return <p className="message">)0ops, something is wrong. </p>
}
if(loading){
return <p className="message">Loading....</p>
}
return (
<div>
{articleList.map(item => {
return <Preview {...item} key={item.id} push={this.props.push} />
})}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
loading: state.list.loading,
error: state.list.error,
articleList: state.list.articleList
}
};
export default connect(mapStateToProps, { loadArticles })(PreviewList);
Also, your code needs some major restructuring, it's really difficult to read through it and see how the different pieces are connected together.

Resources