Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to pass a React Component to puppeteer?

I have a React component with some componentDidMount logic:

export default class MyComponent {
    componentDidMount() {
        // some changes to DOM done here by a library  
    }

    render() {
        return (
            <div>{props.data}</div>
        );
    }
}

Is it possible to pass this component with props so that everything in componentDidMount() gets executed, somehow to puppeteer in order to take a screenshot? Something along these lines:

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

const html = ReactDOMServer.renderToString(<MyComponent data='' />); <-- but this skips the componentDidMount logic
await page.setContent(html);
await page.screenshot({ path: 'screenshot.png' });

I know I could use page.goto(), but I have some complex login logic that I would like to avoid with a shortcut like this and instead pass all the needed props just directly to the component?

like image 289
blub Avatar asked Dec 30 '17 14:12

blub


People also ask

Can you use puppeteer with React?

Jest and Puppeteer are a combination that can't go wrong when it comes to testing React apps.

Can you pass a React component as prop?

You can pass a component as props in React by using the built-in children prop. All elements you pass between the opening and closing tags of a component get assigned to the children prop. Copied!

Can puppeteer be used client side?

Now you can control the browser (running on the server) form the client-side with a puppeteer bundle for the client.

Can props pass JSX?

You want to use JSX inside your props You can simply use {} to cause JSX to parse the parameter. The only limitation is the same as for every JSX element: It must return only one root element.


1 Answers

I answered this question here. Let's try the same here.

Install babel, webpack and puppeteer packages.

{
  "name": "react-puppeteer",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "compile": "webpack",
    "build": "webpack -p",
    "start": "webpack && node pup.js"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "webpack": "^3.10.0",
    "webpack-dev-middleware": "^2.0.3"
  },
  "dependencies": {
    "puppeteer": "^0.13.0"
  }
}

Prepare webpack config,

const webpack = require('webpack');

const loaders = [
  {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    query: {
      presets: ['babel-preset-es2015', 'babel-preset-react'],
      plugins: []
    }
  }
];

module.exports = {
  entry: './entry.js',
  output: {
    path: __dirname,
    filename: 'bundle.js',
    libraryTarget: 'umd'
  },
  module: {
    loaders: loaders
  }
};

Create entry file, On this file, instead of mounting the element directly, export it to window so that we can access it later.

import React from 'react';
import { render } from 'react-dom';

class Hello extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// accept a name for example and a domNode where to render
function renderIt(name, domNode) {
  render(<Hello name={name} />, domNode);
}

window.renderIt = renderIt;

When we run webpack, it's going to produce a bundle.js file. We can use it on puppeteer.

They have deprecated the injectFile function on puppeteer, so we are going to resurrect it. Here is a sample repo for that, you can yarn add it.

https://github.com/entrptaher/puppeteer-inject-file

Now, lets create a puppeteer script.

const puppeteer = require('puppeteer');
const injectFile = require('puppeteer-inject-file');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto('https://github.com');
  await injectFile(page, require.resolve('./bundle.js'));
  await page.evaluate(() => {
    renderIt("Someone", document.querySelector('div.jumbotron.jumbotron-codelines > div > div > div > h1'));
  });
  await page.screenshot({ path: 'example.png' });
  await browser.close();
})();

And when we run this, we get following result,

enter image description here

If we added a componentDidMount() call, we could have done that too. But if we are trying to do more modification, we have to make the puppeteer script wait for that which have been discussed many times in other questions.

Say we have a state now which will return something once component is loaded.

class Hello extends React.Component {
  state = {
    jokes: null
  };

  componentDidMount() {
    const self = this;
    const jokesUrl = `http://api.icndb.com/jokes/random?firstName=John&amp;lastName=Doe`;
    fetch(jokesUrl)
      .then(data => data.json())
      .then(data => {
        self.setState({
          jokes: data.value.joke
        });
      });
  }

  render() {
    if(!!this.state.jokes){
      return <p id='quote'>{this.state.jokes}</p>
    }
    return <h1>Hello, {this.props.name}</h1>;
  }
}

On puppeteer, I can wait for the element like this,

  ...
  await injectFile(page, require.resolve('./bundle.js'));
  await page.evaluate(() => {
    renderIt("Someone", document.querySelector('div'));
  });
  await page.waitFor('p#quote');
  ...

We might need babel-preset-stage-2 but I'll leave that to you. And here is the result,

enter image description here

Figure the rest of the problem yourself :) ...

like image 143
Md. Abu Taher Avatar answered Oct 03 '22 05:10

Md. Abu Taher