Implementation of react-intersection-observer module not working on multiple components - reactjs

I am struggling with the implementation of the "react-intersection-observer" and i can't for the life of me find a solution.
Details:
I have a simple presentation site which i wanna do with React so i can also learn. Right now the website has only the homepage and the homepage has so far these sections: header, about, portfolio and contact form.
What i wanna do is to simply add a class on each section (about, portfolio and contact form) once the section is in viewport. The kind of stuff that with jquery would be over in 2 minutes.
I have installed "react-intersection-observer" and so far the code in my homepage.component.jsx looks like this:
import React from 'react';
import Header from "../../components/header/header.component";
import About from "../../components/about/about.component";
import PortfolioList from "../../components/portfolio-list/portfolio-list.component";
import ContactForm from "../../components/contact-form/contact-form.component";
import { useInView } from 'react-intersection-observer';
const HomePage = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
triggerOnce: true,
threshold: 1,
onChange: (inView, entry) => {
console.log("salam");
console.log(inView);
console.log(entry);
if (inView) {
entry.target.classList.add("in-view");
}
}
});
return (
<div>
<Header/>
<main className="main">
<About />
<PortfolioList />
<ContactForm />
</main>
</div>
);
};
export default HomePage;
When i have added ref={ref} on each component like this:
<About ref={ref} />
<PortfolioList ref={ref} />
<ContactForm ref={ref} />
i have received an error: Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
The thing is that i don't want to add the useInView module in each of the 3 jsx components because it seems bad practice to have repeat code.

Passing ref as props is bad practice.
Use React.forwardRef instead:
https://beta.reactjs.org/apis/react/forwardRef
Check this example:
https://beta.reactjs.org/apis/react/forwardRef#forwarding-a-ref-through-multiple-components

Related

ReactJS -How to create multistep component/form with single path using React Router

I want to switch between components after the user entered the requested info.
Components that will be shown to user by this order:
{MobileNum } Enter mobile number
{IdNumber } ID number
{CreatePassword } Create Password
When all these steps are completed the browser will switch to the home page.
The user must not be able to move between pages until he filled each request in each component.
Now I want a better way with router as if I had 3-4 components inside Login, and must be in a secured whey, also the user must not be able to switch components manually through the URL.
import React, { Component } from 'react';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
import MobileNum from './MobileNum.jsx';
import IdNumber from './IdNum.jsx';
import CreatePassword from './createPassword .jsx';
class SignUp extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
//// Here needs to know how to navigate to each component on its turn
<Route path='/' component={MobileNum} />
<Route path='/' component={IdNumber} />
<Route path='/' component={CreatePassword } />
</Switch>
</Router>
</div>
);
}
}
export default SignUp ;
I searched the web in reactrouter.com and many others as here for a clean solution but found no answer.
Any Idea what's the best way to do it ?
Thanks
Since router variable like location are immutable, conditional rendering itself would be better option, you can try switch if you don't want to use if else.
I have given an example below, you have to fire that afterSubmit when values are submitted in each component .If you use redux, you could implement it better as you can store the value in redux state and set it directly from each component using dipatch.
//App.js
import React, { useState } from 'react';
import MobileNum from './MobileNum.jsx';
import IdNumber from './IdNum.jsx';
import CreatePassword from './createPassword .jsx';
function App (){
const [stage,setStage]= useState(1);
switch(stage){
case 2:
return <IdNumber afterSubmit={setStage.bind(null,3)}/>
break;
case 3:
return <CreatePassword afterSubmit={setStage.bind(null,4)} />
case 4:
return <Home />
break;
default:
return <MobileNum afterSubmit={setStage.bind(null,2)}/>
}
}
export default App;
//Root
import React, { Component } from 'react';
import App from './App.jsx';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
class Login extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
<Route path='/' component={App} />
</Switch>
</Router>
</div>
);
}
}
//Add on - Sign up form class based
class SignUp extends React.Component {
constructor(props) {
super(props);
this.state = { stage: 1 };
}
render() {
switch (this.state.stage) {
case 2:
return <IdNumber afterSubmit={() => this.setState({ stage: 3 })} />;
break;
case 3:
return <CreatePassword afterSubmit={() => this.setState({ stage: 4 })} />;
case 4:
return <Home />;
break;
default:
return <MobileNum afterSubmit={() => this.setState({ stage: 2 })} />;
}
}
}
It will take special handling in React Router to meet your security requirements. I personally would load the multi-step wizard on one URL rather than changing the URL for each step as this simplifies things and avoids a lot of potential issues. You can get the setup that you want, but it is much more difficult than it needs to be.
Path-Based Routing
I am using the new React Router v6 alpha for this answer, as it makes nested routes much easier. I am using /signup as the path to our form and URLs like /signup/password for the individual steps.
Your main app routing might look something like this:
import { Suspense, lazy } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
const Home = lazy(() => import("./Home"));
const MultiStepForm = lazy(() => import("./MultiStepForm/index"));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<BrowserRouter>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup/*" element={<MultiStepForm/>} />
</Routes>
<Footer />
</BrowserRouter>
</Suspense>
);
}
You'll handle the individual step paths inside the MultiStepForm component. You can share certain parts of the form across all steps. The part which is your Routes should just be the part that is different, ie. the form fields.
Your nested Routes object inside the MultiStepForm is essentially this:
<Routes>
<Route path="/" element={<Mobile />} />
<Route path="username" element={<Username />} />
<Route path="password" element={<Password />} />
</Routes>
But we are going to need to know the order of our route paths in order to handle "Previous" and "Next" links. So in my opinion it makes more sense to generate the routes based on a configuration array. In React Router v5 you would pass your config as props to a <Route/>. In v6 you can skip that step and use object-based routing.
import React, { lazy } from "react";
const Mobile = lazy(() => import("./Mobile"));
const Username = lazy(() => import("./Username"));
const Password = lazy(() => import("./Password"));
/**
* The steps in the correct order.
* Contains the slug for the URL and the render component.
*/
export const stepOrder = [
{
path: "",
element: <Mobile />
},
{
path: "username",
element: <Username />
},
{
path: "password",
element: <Password />
}
];
// derived path order is just a helper
const pathOrder = stepOrder.map((o) => o.path);
Note that these components are called with no props. I am assuming that they get all of the information that they need through contexts. If you want to pass down props then you will need to refactor this.
import { useRoutes } from "react-router-dom";
import { stepOrder } from "./order";
import Progress from "./Progress";
export default function MultiStepForm() {
const stepElement = useRoutes(stepOrder);
return (
<div>
<Progress />
{stepElement}
</div>
);
}
Current Position
This is the part where things start to become convoluted. It seems that useRouteMatch has been removed in v6 (for now at least).
We can access the matched wildcard portion on the URL using the "*" property on the useParams hook. But this feels like it might be a bug rather than an intentional behavior, so I'm concerned that it could change in a future release. Keep that in mind. But it does work currently.
We can do this inside of a custom hook so that we can derive other useful information.
export const useCurrentPosition = () => {
// access slug from the URL and find its step number
const urlSlug = useParams()["*"]?.toLowerCase();
// note: will be -1 if slug is invalid, so replace with 0
const index = urlSlug ? pathOrder.indexOf(urlSlug) || 0 : 0;
const slug = pathOrder[index];
// prev and next might be undefined, depending on the index
const previousSlug = pathOrder[index - 1];
const nextSlug = pathOrder[index + 1];
return {
slug,
index,
isFirst: previousSlug === undefined,
isLast: nextSlug === undefined,
previousSlug,
nextSlug
};
};
Next Step
The user must not be able to move between pages until he filled each request in each component.
You will need some sort of form validation. You could wait to validate until the user clicks the "Next" button, but most modern websites choose to validate the data every time that the form changes. Packages like Formik and Yup are a huge help with this. Check out the examples in the Formik Validation docs.
You will have an isValid boolean which tells you when the user is allowed to move on. You can use that to set the disabled prop on the "Next" button. That button should have type="submit" so that its clicks can be handled by the onSubmit action of the form.
We can make that logic into a PrevNextLinks component which we can use in each form. This component uses the formik context so it must be rendered inside of a <Formik/> form.
We can use the info from our useCurrentPosition hook to render a Link to the previous step.
import { useFormikContext } from "formik";
import { Link } from "react-router-dom";
import { useCurrentPosition } from "./order";
/**
* Needs to be rendered inside of a Formik component.
*/
export default function PrevNextLinks() {
const { isValid } = useFormikContext();
const { isFirst, isLast, previousSlug } = useCurrentPosition();
return (
<div>
{/* button links to the previous step, if exists */}
{isFirst || (
<Link to={`/form/${previousSlug}`}>
<button>Previous</button>
</Link>
)}
{/* button to next step -- submit action on the form handles the action */}
<button type="submit" disabled={!isValid}>
{isLast ? "Submit" : "Next"}
</button>
</div>
);
}
Here's an example of how one step might look:
import { Formik, Form, Field, ErrorMessage } from "formik";
import React from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import Yup from "yup";
import "yup-phone";
import PrevNextLinks from "./PrevNextLinks";
import { useCurrentPosition } from "./order";
import { saveStep } from "../../store/slice";
const MobileSchema = Yup.object().shape({
number: Yup.string()
.min(10)
.phone("US", true)
.required("A valid US phone number is required")
});
export default function MobileForm() {
const { index, nextSlug, isLast } = useCurrentPosition();
const dispatch = useDispatch();
const navigate = useNavigate();
return (
<div>
<h1>Signup</h1>
<Formik
initialValues={{
number: ""
}}
validationSchema={MobileSchema}
validateOnMount={true}
onSubmit={(values) => {
// I'm iffy on this part. The dispatch and the navigate will occur simoultaneously,
// so you should not assume that the dispatch is finished before the target page is loaded.
dispatch(saveStep({ values, index }));
navigate(isLast ? "/" : `/signup/${nextSlug}`);
}}
>
{({ errors, touched }) => (
<Form>
<label>
Mobile Number
<Field name="number" type="tel" />
</label>
<ErrorMessage name="number" />
<PrevNextLinks />
</Form>
)}
</Formik>
</div>
);
}
Preventing Access via URL
The user must not be able to switch components manually through the URL.
We need to redirect the user if they attempt to access a page which they are not permitted to view. The Redirects (Auth) example in the docs should give you some ideas on how this is implemented. This PrivateRoute component in particular:
// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
But what is your equivalent version of useAuth?
Idea: Look at the Current Progress
We could allow the visitor to view their current step and any previously entered steps. We look to see if the user is allowed to view the step which they are attempting to access. If yes, we load that content. If no, you can redirect them to their correct step or to the first step.
You would need to know what progress has been completed. That information needs to exist somewhere higher-up in the chain like localStorage, a parent component, Redux, a context provider, etc. Which you choose is up to you and there will be some differences. For example using localStorage will persist a partially-completed form while the others will not.
Where you store is less important that What you store. We want to allow backwards navigation to previous steps and forwards navigation if going to a previously-visited step. So we need to know which steps we can access and which we can't. The order matters so we want some sort of array. We would figure out the maximum step which we are allowed to access and compare that to the requested step.
Your component might look like this:
import { useRoutes, Navigate } from "react-router-dom";
import { useSelector } from "../../store";
import { stepOrder, useCurrentPosition } from "./order";
import Progress from "./Progress";
export default function MultiStepForm() {
const stepElement = useRoutes(stepOrder);
// attempting to access step
const { index } = useCurrentPosition();
// check that we have data for all previous steps
const submittedStepData = useSelector((state) => state.multiStepForm);
const canAccess = submittedStepData.length >= index;
// redirect to first step
if (!canAccess) {
return <Navigate to="" replace />;
}
// or load the requested step
return (
<div>
<Progress />
{stepElement}
</div>
);
}
CodeSandbox Link. (Note: Most of the code in the three step forms can and should be combined).
This is all getting rather complicated, so let's try something simpler.
Idea: Require that the URL Be Accessed from a Previous/Next Link
We can use the state property of a location to pass through some sort of information that lets us know that we've come from the correct place. Like {fromForm: true}. Your MultiStepForm can redirect all traffic that lacks this property to the first step.
const {state} = useLocation();
if ( ! state?.fromForm ) {
return <Navigate to="" replace state={{fromForm: true}}/>
}
You would make sure that all of your Link and navigate actions inside of the form are passing this state.
<Link to={`/signup/${previousSlug}`} state={{fromForm: true}}>
<button>Previous</button>
</Link>
navigate(`/signup/${nextSlug}`, {state: { fromForm: true} });
With No Path Change
After having written quite a lot of code and explanation about authenticating a path, I've realized that you haven't explicitly said that the path needs to change.
I just need to use react-router-dom properties to navigate.
So you could make use of the state property on the location object to control the current step. You pass the state through your Link and navigate the same as above, but with an object like {step: 1} instead of {fromForm: true}.
<Link to="" replace state={{step: 2}}>Next</Link>
You can majorly simplify your code by doing this. Though we come back to a fundamental question of why. Why use React Router if the important information is a state? Why not just use a local component state (or Redux state) and call setState when you click on the "Next" button?
Here's a good article with a fully-implemented code example using local state and the Material UI Stepper component:
Build a multi-step form with React Hooks, Formik, Yup and MaterialUI by Vu Nguyen
CodeSandbox
When all these steps are completed the browser will switch to the home page.
There are two ways to handle your final redirect to the home page. You can conditionally render a <Navigate/> component (<Redirect/> in v5) or you can call navigate() (history.push() in v5) in response to a user action. Both versions are explained in detail in the React Router v6 Migration guide.
I don't think adding React Router or any library changes the way how we solve a problem in React.
Your earlier approach was fine. You could wrap all it in a new component, like,
class MultiStepForm extends React.Component {
constructor(props) {
super(props);
this.state = {
askMobile: true,
};
};
askIdentification = (passed) => {
if (passed) {
this.setState({ askMobile: false });
}
};
render() {
return (
<div className='App-div'>
<Header />
{this.state.askMobile ? (
<MobileNum legit={this.askIdentification} />
) : (
<IdNumber />
)}
</div>
);
}
}
Then use this component on your Route.
...
<Switch>
<Route path='/' component={MultiStepForm} />
// <Route path='/' component={MobileNum} />
// <Route path='/' component={IdNumber} />
// <Route path='/' component={CreatePassword } />
</Switch>
...
Now how you'd like to move on with this is a completely new question.
Also, I have corrected the spelling of askIdentification.

React-router custom prop not passing to component. ternary operator not working correctly

In React i have my App.js page where i keep my states. I'm importing user1.js component to App.js, and in user1.js component i have a link button that takes me to path /user2.
When i click the button, React will set state property called testValue to true and in user2.js page ternary operator should choose the first value - test works because of that. But for some reason it does not work.
Any help?
APP.JS
import React, { Component } from 'react';
import './App.css';
import User1 from './components/user1';
class App extends Component {
constructor(props){
super(props);
this.state = {
testValue:false
};
}
change = () => {
this.setState({
testValue:true
},() => {
console.log(this.state.testValue)
});
}
render() {
return (
<div className="App">
<User1 change={this.change}/>
</div>
);
}
}
export default App;
USER1.JS
import React from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
import User2 from './user2.js';
const User1 = (props) => {
return(
<BrowserRouter>
<div>
<Link to ="/user2">
<button onClick={props.change}>Next page</button>
</Link>
<Switch>
<Route path="/user2" exact component={User2}/>
</Switch>
</div>
</BrowserRouter>
); // end of return
};
export default User1;
USER2.JS
import React from 'react';
const User2 = (props) => {
console.log(props)
return(
<div>
{props.testValue ?
<p>test works</p>
:
<p>test does not work</p>
}
</div>
);
};
export default User2;
This is what i expected - test works
This is what i got - test does not work
You want to pass a custom property through to a component rendered via a route. Recommended way to do that is to use the render method.
<Route path="/user2" exact render={(props) => <User2 {...props} testValue={true} />} />
I think a valid inquiry here would be what are you wanting to pass through as an extra prop? whats the use case here? You may be trying to pass data in a way you shouldn't (context would be nice :D).

Change the components only after data received from 3rd party in react router

How does Youtube, Instagram, Github change the content only after they receive data?
<Switch>
...Other Route
<Route path = "/" exact component={HomeComp} />
<Route path = "/articles" component={ArticleComp} />
</Switch>
In my knowledge when I click a Nav Link to replace url from / to /articles the component replace from HomeComp to ArticleComp as well. But what I saw from other SPA application(those I mention above) even though the url is replace but the components aren't replace instead there is an progress bar, components are replace only until receiving response from fetch request. If you can't understand my word I try to include a picture for better understanding
If I want to do something like that where should I perform fetch request? From the doc It say it should perform in componentsDidMount(). But it seem not right since the component wasn't initial until the data is loaded.
Very simple question how can achieve the goal? Replace components only after receiving fetch response rather than replace url > replace components > start fetch request. The solution I seek for is like how github,youtube do(photo below).
Can I still stick with react-router if I want to achieve this?
Sorry for keep repeating the same question, I was worry what I ask is not clear. English is not my primary language so it is really hard for me research, I don't know include what keyword to find the correct solution. If this question is asked before kindly include the link for me. Thank you!
So, the assumption here is that you want certain parts of your UI common across different pages. i.e... HomeComp and ArticleComp.
Imagine that you have a Layout component (container):
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Header from './Header';
import Footer from './Footer';
class Layout extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
children: PropTypes.node,
};
render() {
return (
<div className={_className}>
<Header />
<div className={_rhsContentClassName}>
{ this.props.children }
</div>
<Footer />
</div>
);
}
}
export default Layout;
Now, in your routes file, you describe the routes as,
<Switch>
...Other Route
<Route path = "/" exact component={HomeComp} />
<Route path = "/articles" component={ArticleComp} />
</Switch>
and for each of the Route Component, HomeComp or ArticleComp, your react component should look something like this:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Layout from './Layout';
import Preloader from './Preloader';
class ArticleComp extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
};
componentWillMount() {
this.setState({ isLoading: true };
actions
.fetchData()
.then(() => {
// do something
// store data for the content
this.setState({ isLoading: false };
});
}
render() {
if (isLoading)
return <Preloader />;
return (
<Layout>
<div> {/* content based on the fetch request */}</div>
</Layout>
);
}
}
export default ArticleComp;
This way, you get to segregate your Header, Footer or other static ( or even dynamic concerns ) with the real request-based content.

React Component Mounting Twice

Inside a small portion of my React/Redux/ReactRouterV4 application, I have the following component hierarchy,
- Exhibit (Parent)
-- ExhibitOne
-- ExhibitTwo
-- ExhibitThree
Within the children of Exhibit, there are about 6 different possible routes that can be rendered as well. Don't worry, I will explain with some code.
Here is my Parent Exhibit Component:
export class Exhibit extends Component {
render() {
const { match, backgroundImage } = this.props
return (
<div className="exhibit">
<Header />
<SecondaryHeader />
<div className="journey"
style={{
color: 'white',
backgroundImage: `url(${backgroundImage})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center-center'
}}>
<Switch>
<Route path={`${match.url}/exhibit-one`} component={ExhibitOne} />
<Route path={`${match.url}/exhibit-two`} component={ExhibitTwo} />
<Route path={`${match.url}/exhibit-three`} component={ExhibitThree} />
<Redirect to="/" />
</Switch>
</div>
</div>
)
}
}
Basically, all its does for its job is to display one of the exhibits subcomponents, and set a background image.
Here is one of the subcomponents, ExhibitOne:
export default class ExhibitOne extends Component {
constructor(props) {
super(props)
}
render() {
const { match } = this.props
return (
<div className="exhibit-one">
<Switch>
<Route path={`${match.url}/wall-one`} component={ExhibitHOC(WallOne)} />
<Route path={`${match.url}/wall-two`} component={ExhibitHOC(WallTwo)} />
<Route path={`${match.url}/wall-three`} component={ExhibitHOC(WallThree)} />
<Route path={`${match.url}/wall-four`} component={ExhibitHOC(WallFour)} />
<Route path={`${match.url}/wall-five`} component={ExhibitHOC(WallFive)} />
<Route path={`${match.url}/wall-six`} component={ExhibitHOC(WallSix)} />
</Switch>
</div>
)
}
}
In order to cut down on typing, I decided to wrap the components in a Higher Order Component, whose
purpose is to dispatch an action that will set the proper background image on the top level Exhibit parent component.
This is the Higher Order Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions/wall-background-image'
export default function(ComposedComponent) {
class ExhibitHoc extends Component {
componentDidMount = () => this.props.setBackgroundImage(`./img/exhibit-one/${this.getWall()}/bg.jpg`)
getWall = () => {
// this part isnt important. it is a function that determines what wall I am on, in order to set
// the proper image.
}
render() {
return <ComposedComponent />
}
}
return connect(null, actions)(ExhibitHoc);
}
On initial load of ExhibitOne, I can see that the setBackgroundImage action creator executes twice by looking
at Redux Logger in the console. My initial inclination to use componentDidMount was because I thought using it
would limit the action creator to execute only once. Here is a screenshot of the log:
I think I might be misunderstanding how Higher Order Components work, or maybe its some type of React Router V4 thing?
Anyways, any help would be greatly appreciated as to why this executes twice.
Here in 2020, this was being caused by <React.StrictMode> component that was wrapped around the <App /> in new versions of Create React App. Removing the offending component from index.js fixed the double mount problem for all of my components. This was by design, but it was annoying and misleading to see console.log() twice for everything.
For Next.js, in next.config.js, set reactStrictMode:false.
The problem is that the component prop here is a function application, which yields a new class on each render. This will cause the previous component to unmount and the new one to mount (see the docs for react-router for more information). Normally you would use the render prop to handle this, but this won't work with higher-order components, as any component that is created with a HOC application during rendering will get remounted during React's reconciliation anyway.
A simple solution is to create your components outside the ExhibitOne class, e.g.:
const ExhibitWallOne = ExhibitHOC(WallOne);
const ExhibitWallTwo = ExhibitHOC(WallTwo);
..
export default class ExhibitOne extends Component {
..
<Route path={`${match.url}/wall-one`} component={ExhibitWallOne} />
<Route path={`${match.url}/wall-two`} component={ExhibitWallTwo} />
..
}
Alternatively, depending on what the wrapper does, it might be possible to declare it as a normal component that renders {this.props.children} instead of the parameter <ComposedComponent/>, and wrap the components in each Route:
<Route path={`${match.url}/wall-one`}
render={(props) => <Wrap><WallOne {...props}/></Wrap>}
/>
Note that you'll need to use render instead of component to prevent remounting. If the components don't use routing props, you could even remove {...props}.
If you use 'Hidden Material UI React', it mounts your component every time you call it. For example, I wrote the below one:
<Hidden mdDown implementation="css">
<Container component="main" maxWidth="sm">
{content}
</Container>
</Hidden>
<Hidden smUp implementation="css">
{content}
</Hidden>
It invokes both contents in both hidden components. it took me a lot of time.

React: Is it bad practice to import a child component directly rather than pass in as a dependency?

I may be over thinking this, but I am curious if importing a child component directly is bad practice with regards to coupling and testing.
Below is a simple example:
import Header from './header.jsx';
class Widget extends React.Component {
render() {
return (
<div>
<Header></Header>
<div>{this.props.importantContent}</div>
</div>
)
}
}
To me it looks like there is now coupling between Widget and Header. With regards to testing, I don't see an easy way to mock the Header component when testing the Widget component.
How do other larger React apps handle cases like this? Should I pass Header in as a prop? If using react-redux, I can inject header with the Connect method like below to reduce boilerplate. Is that sound?
import { connect } from 'react-redux';
import Header from './header.jsx';
class Widget extends React.Component {
render() {
return (
<div>
{this.props.header}
<div>{this.props.importantContent}</div>
</div>
)
}
}
const mapStateToProps = state => {
return {
header: Header
}
}
export default connect(mapStateToProps)(Widget)
I am interested is simple doing what the community is generally doing. I see that one solution is doing shallow rendering to test on the main part of the component and not the child components using something like Enzyme.
Thoughts or other ideas?
Passing elements / components as props is a good idea. Having default props is a good idea too:
const Widget = ({
header = <div>Default Header.. </div>,
content = <div>Default Content.. </div>
}) =>
<div>
{header}
{content}
</div>
Then elsewhere in your app:
<Widget header={<Header title="Foo" />} content="content from props" />
No need to inject using connect
You can also pass a component, not just an element if you want to interact with props / send data back to parent:
const Widget = ({
Header = props => <div>Default Header.. </div>,
Content = props => <div>Default Content.. </div>
}) =>
<div>
<Header />
<Content />
</div>
Elsewhere:
<Widget Header={Header} Content={props => <Content />} />
As long as the component always renders the same thing it can be directly rendered as a child rather than the parent.
If all other portions of the Component remain constant and only the Header can be different across pages then you could actually implement it as an HOC instead of passing it as a props
const MyCompFactory = ({CustomHeader = DefaultHeader}) => {
return class Widget extends React.Component {
render() {
return (
<div>
<CustomHeader/>
<div>{this.props.importantContent}</div>
</div>
)
}
}
}
and use it like
const CustomComponent = MyCompFactory({CustomComponent: Header})
as long as testing is concerned in your case, you could just shallow render your component and then Search if the Header component is rendered something like
import Header from 'path/to/header'
const component = shallow(
<Widget {...customProps}/>
)
test('test' , () => {
expect(component.find(Header).exists()).toBe(true)
})

Resources