react combine differents wrappers - javascript

We are starting a new project in React.
And we need to use:
React Context API
i18n (react.i18nex)
GraphQL (Apollo client)
Redux
CCS-in-JS (styled-components or aphodite)
The thing is that any of this implementation wraps a component and pass it some props or use functions as children.
And I want to keep my components as decoupled and clean as possible.
How can I structure the wrappers?
Example
Home
<Home {...props} />
Context:
<ThemeContext.Consumer>
{ theme=> <Home {...theme} programa={theme} /> }
</ThemeContext.Consumer>
i18n:
<I18n>
{t => <Home text={t("translated text")} /> }
</I18n>
GraphQL:
<Query query={GET_PROGRAMA}>
{({ data }) => <Home data={data} />}
</Query>
Redux:
const mapStateToProps = state => ({
user: "some user"
});
export default connect(
mapStateToProps,
)(Home);
As you can see, the Home component receives isolated props from many sources.
How can I manage ir and keep it decoupled? There is some kind of composer?
Thank you!

I think you can convert this into an HOC that will handle all of the component wrapping for you:
const withWrappers = WrappedComponent => {
return class extends React.Component {
render () {
return (
<ThemeContext.Consumer>
{ theme =>
<I18n>
{ t =>
<Query query={GET_PROGRAMA}>
{ ({ data }) =>
<WrappedComponent
{...this.props}
{...theme}
programa={theme}
data={data}
text={t("translated text")}
/>
}
</Query>
}
</I18n>
}
</ThemeContext.Consumer>
)
}
}
}
Usage:
class Home extends React.Component {
render () {
return (
<div>Home</div>
)
}
}
export default withWrappers(Home);

Thanks for your help! All of you.
I've implemented a HOC, the same as you porposed to me, and keep the GraphQL components outside:
import React from "react";
import PropTypes from "prop-types";
import { mergeStyles } from "js/utils";
import { I18n } from "react-i18next";
import { TemplateContext } from "js/template-context";
const Wrapper = ({ Component, getStylesFromTemplate }) => props => {
const { classes } = props;
return (
<TemplateContext.Consumer>
{programa => {
const { template } = programa;
const stylesFromTemplate = getStylesFromTemplate(template);
const styles = mergeStyles({ classes, stylesFromTemplate });
return (
<I18n>
{(t, { i18n }) => {
return (
<Component
t={t}
styles={styles}
programa={programa}
{...props}
/>
);
}}
</I18n>
);
}}
</TemplateContext.Consumer>
);
};
Wrapper.propTypes = {
classes: PropTypes.object.isRequired
};
export default Wrapper;
Again, thank you so much!

Related

Why is my component unable to access data from my reducer?

I am writing a React app in which somebody can sign up as a business or user, and a user is able to search for a business by name. I do not understand why I am getting an error when trying to render my search component, saying "TypeError: Cannot read properties of undefined (reading 'toLowerCase')". I do not understand why I am getting this error because I believe I am passing in the appropriate data via my reducers and the Redux store. This is my search component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import BusinessCard from '../Business/BusinessCard'
import { Card } from 'semantic-ui-react';
class Businesses extends Component {
state = {newSearch: ""}
handleInputChange = e => {
this.setState({newSearch: e.target.value})
}
render() {
const { businessesReducer} = this.props;
let businessesMatch = businessesReducer.businesses.filter( (business ) => business.name.toLowerCase().includes(this.state.newSearch.toLowerCase()))
return (
<div>
<input placeholder="Search Events and Services Near You" value={this.state.newSearch} name="businessName" type="text" onChange={this.handleInputChange} />
<Card.Group itemsPerRow={3}>
{ businessesMatch.map((business, id) => <BusinessCard key={id} business={business} />)}
</Card.Group>
</div>
)
}
}
const mapStateToProps = (state) => {
return ({
businessesReducer: state.businessesReducer
})
}
export default connect(mapStateToProps)(Businesses);
My businesses reducer:
const initialState =
{
businesses:[],
isLoading: false
}
export default (state = initialState, action) => {
switch (action.type) {
case 'LOADING':
return {
...state,
isLoading: true
}
case "GET_ALL_BUSINESSES_SUCCESS":
return { ...state,
businesses: action.businesses,
isLoading: false
}
default:
return state
}
}
BusinessCard.js (which I am trying to render per the user's search)
import React, { Component } from 'react';
import { Card } from 'semantic-ui-react';
import { connect } from 'react-redux';
class BusinessCard extends Component {
constructor(props) {
super(props);
}
render(){
const { business, businessesReducer } = this.props;
return(
<Card>
<div key={business.id} >
<Card.Content>
<Card.Header><strong>{business.name}</strong></Card.Header>
</Card.Content>
</div>
</Card>
)
}
}
const mapStateToProps = state => {
return {
businesses: state.businesses,
businessesReducer: state.businessesReducer
}
}
export default connect(mapStateToProps)(BusinessCard);
And App.js
import { getAllBusinesses } from './actions/business/business';
import { BrowserRouter as Router, Route, Switch} from 'react-router-dom';
import history from './history';
class App extends React.Component {
componentDidMount() {
this.props.getAllBusinesses();
}
render() {
return (
<Router history={history}>
<div className="App">
<NavBar />
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About} />
<Route path="/services" component={Services} />
<Route path="/shop" component={Shop}/>
<Route path="/login-signup" component={LoginContainer}/>
<Route path="/signup" component={Signup}/>
<Route path="/business-signup" component={BusinessSignup}/>
<Route path="/professional-signup" component={ProfessionalSignup}/>
<Route path="/search" component={Businesses}/>
</Switch>
</div>
</Router>
)
}
}
const mapStateToProps = (state) => {
return {
businessesReducer: state.businessesReducer
}
}
export default connect(mapStateToProps, {getAllBusinesses})(App);
Does anybody have any idea why my search component cannot access "business" and its properties? Everything looks correct to me.
1: It would be good if you could show getAllBusinesses.
2: Please make sure if data exists in your store, you can use redux-dev-tools for that.
3: The first time that your component renders there is no data in your store and it's just an empty array so please first check if name exists and has value then try to convert it to lower case.
It would be something like this:
let businessesMatch = businessesReducer.businesses.filter(
(business) =>
business.name &&
business.name
.toLowerCase()
.includes(this.state.newSearch.toLowerCase())
);
Or if with optional chaining:
let businessesMatch = businessesReducer.businesses.filter((business) =>
business?.name
.toLowerCase()
.includes(this.state.newSearch.toLowerCase())
);
If none of these help please provide more information like a code sandbox.

How do I turn a JSX Element into a Function Component in React?

My React app has the following in App.js:
const App = () => (
<Router>
<Switch>
... various routes, all working fine ...
<Route exact path={ROUTES.DASHBOARD} render={(props) => <Dashboard {...props} />}/>
</Switch>
</Router>
);
I'm getting an error on Dashboard, which says JSX element type 'Dashboard' does not have any construct or call signatures.
This is because Dashboard is created like this:
const DashboardPage = ({firebase}:DashboardProps) => {
return (
<div className="mainRoot dashboard">...contents of dashboard...</div>
);
}
const Dashboard = withFirebase(DashboardPage);
export default Dashboard;
and withFirebase is:
import FirebaseContext from './firebaseContext';
const withFirebase = (Component:any) => (props:any) => (
<FirebaseContext.Consumer>
{firebase => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
export default withFirebase;
So withFirebase is exporting a JSX element, so that's what Dashboard is. How can I ensure that withFirebase is exporting a Component instead?
So withFirebase is exporting a JSX element, so that's what Dashboard is. How can I ensure that withFirebase is exporting a Component instead?
withFirebase is not creating a JSX element, it is creating a function which creates a JSX Element -- in other words that's a function component. Perhaps it helps to type it properly.
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase | null }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
Those type are explained in detail in this answer. Is your context value sometimes null? Can your DashboardPage handle that, or do we need to handle it here? Here's one way to make sure that DashboardPage can only be called with a valid Firebase prop.
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) =>
firebase ? (
<Component {...props} firebase={firebase} />
) : (
<div>Error Loading Firebase App</div>
)
}
</FirebaseContext.Consumer>
);
Now that we have fixed the HOC, your Dashboard component has type React.FC<{}>. It's a function component that does not take any props.
You do not need to create an inline render method for your Route (this will actually give errors about incompatible props). You can set it as the component property component={Dashboard}.
complete code:
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
// placeholder
class Firebase {
app: string;
constructor() {
this.app = "I'm an app";
}
}
const FirebaseContext = React.createContext<Firebase | null>(null);
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) =>
firebase ? (
<Component {...props} firebase={firebase} />
) : (
<div>Error Loading Firebase App</div>
)
}
</FirebaseContext.Consumer>
);
interface DashboardProps {
firebase: Firebase;
}
const DashboardPage = ({ firebase }: DashboardProps) => {
console.log(firebase);
return <div className="mainRoot dashboard">...contents of dashboard...</div>;
};
const Dashboard = withFirebase(DashboardPage);
const App = () => {
const firebase = new Firebase();
return (
<FirebaseContext.Provider value={firebase}>
<Router>
<Switch>
<Route component={Dashboard} />
</Switch>
</Router>
</FirebaseContext.Provider>
);
};
export default App;

React Router v4 Nested Routes pass in match with Class Component

I am trying to have a nested route in my application. Where a component is declared with class syntax.
How do I pass match?
As you can see below, I am using the componentDidMount() function to pull in data, so I need to have the member function and I want this component to handle all my logic.
import React, { Component } from 'react';
import ListItem from './ListItem';
import Post from './Post';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
//holds the state for all the components
//passes into listing
//listing will re-direct to proper post using router
export default class Blog extends Component {
constructor(props){
super(props);
this.state = {
articles: [],
content: null
};
}
storeData = (data) => {
const articles = data.map((post, index) => {
return {
key: index,
title: post.title.rendered,
content: post.content.rendered,
excerpt: post.excerpt.rendered,
slug: post.slug
};
});
this.setState(
{
articles: articles
}
);
};
componentDidMount() {
let articles = [];
fetch('https://XXXXX.com/posts/')
.then(data => data.json())
.then(this.storeData).catch(err => console.log(err));
}
render(){
return(
<div className="blog">
<h2> Listings </h2>
{ this.state.articles.map(post => (
<Link to= { `path/${post.slug}` } >
<ListItem
key={post.key}
title={post.title}
content={post.content}
/>
</Link>
))
}
<Route path='posts/:slug' component={Post} />
</div>
);
}
}
Found it out!
If you look below in render, it was saved as a this.props!
However, now it renders the component below rather than replace to another page.
render(){
return(
<div className="blog">
<h2> Listings </h2>
{ this.state.articles.map(post => (
<Link to={ `${this.props.match.url}/${post.slug}` } >
<ListItem
key={post.key}
title={post.title}
content={post.content}
/>
</Link>
))
}
<Route path={ `${this.props.match.path}/:slug` } component={Post} />
</div>
);
}
}

connect react redux HOC got error of `Cannot call a class as a function`

What's wrong with my HOC below? I got error of cannot call a class as function?
https://i.imgur.com/SirwcGZ.png
My HOC
const CheckPermission = (Component) => {
return class App extends Component {
componentDidMount() {
this.props.fetchUsers().then(resp => {
this.setState({user: true, loading: false});
})
}
render() {
const { user, loading } = this.props
loading && <div>Loading...</div>
!user && <Redirect to="/dashboard" />
return <Component {...this.props} />
}
}
}
export default connect(state=>state.global, {fetchUsers})(CheckPermission)
This is how I import and user CheckPermission:
<Route exact path='/dashboard' component={CheckPermission(Dashboard)} />
you can't wrap checkPermissions with a connect because it is also a HOC.
Instead you have to compose them.
import { compose } from 'redux';
...
export default compose(
connect(state=>state.global, {fetchUsers}),
CheckPermission
);

How to pass component callback to children element in react-router?

I would like to wrap my current router by Layout with one MapBox and pass to children MapBox callback function:
<Route path='/posts' component={PostsList} />
<Route path="/posts/:id" component={Post} >
</Route>
Currently to handle callback I repeat MapBox on two components:
export default class Post extends React.Component {
mapMoved(map) {console.log('map moved', map)}
render() {
const {post, posts} = this.props;
return (
<div>
{post.title}
<MapBox mapMoved={::this.mapMoved} posts={posts} />
</div>
);
}
}
const mapStateToProps = (state) => ({
post: state.post,
posts: state.posts,
});
export default connect(mapStateToProps)(Post);
export default class PostsList extends React.Component {
renderList() {
const {posts} = this.props;
}
mapMoved(map) {console.log('map moved', map)}
render() {
return (
<div>
{::this.renderList()}
<MapBox mapMoved={::this.mapMoved} posts={this.props.posts} />
</div>
);
}
}
const mapStateToProps = (state) => ({
posts: state.posts,
});
export default connect(mapStateToProps)(PostsList);
Desired routes wrapped by Layout:
<Route component={Layout}>
<Route path='/posts' component={PostsList} />
<Route path="/posts/:id" component={Post} >
</Route>
</Route>
Desired Layout component:
export default class Layout extends React.Component {
mapMoved(map) {}
render() {
return (
<div>
{this.props.children} //I would like to pass mapMoved callback here
<MapBox mapMoved={::this.mapMoved} posts={this.props.posts} />
</div>
);
}
}
const mapStateToProps = (state) => ({
posts: state.posts,
});
export default connect(mapStateToProps)(Layout);
I'm using Redux, maybe should I pass somehow callback function trough it?
You can pass props to children like so
export default class Layout extends React.Component {
mapMoved(map) {}
render() {
return (
<div>
{React.cloneElement(this.props.children || <div />, {mapMoved: (map) => this.mapMoved(map)})} //I would like to pass mapMoved callback here
<MapBox mapMoved={::this.mapMoved} posts={this.props.posts} />
</div>
);
}
}
const mapStateToProps = (state) => ({
posts: state.posts,
});
export default connect(mapStateToProps)(Layout);

Categories

Resources