ChumiestBucket
ChumiestBucket

Reputation: 1066

Home component only loads on refresh (not on initial load), race condition?

I am having a situation where, when loading the page for the first time, the images that should be loaded inside Home.js aren't loaded, until refresh is pushed on the browser (Chrome). This is happening in development server and live production server, and sometimes one must hit refresh multiple times before images show up.

Please note that I am new to all web development as well as React as most of my experience is in Android development.

I'm thinking it's something to do with the way I load the images onto the Home component. With logging I found that the images were loaded before the page was loaded (used componentDidMount()), so I'm looking for any solutions available.

Thanks and here's the code...

Main.js:

import React from 'react';
import { withRouter, Route, NavLink, HashRouter } from 'react-router-dom';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInstagram, faGithub, faFacebook } from '@fortawesome/fontawesome-free-brands';
import 'bootstrap/dist/css/bootstrap.css';
import Favicon from 'react-favicon';

import Home from './Home';
import Contact from './Contact';
import socialMediaLinks from './utilities/Utils';

class Main extends React.Component {
    constructor() {
        super();
        this.state = { 
            screenWidth: null,
            isMobile: false,
        };
        this.handleResize = this.handleResize.bind(this);
    }

    componentDidMount() {
        window.addEventListener('resize', this.handleResize.bind(this));
        this.handleResize();
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.handleResize);
    }

    handleResize() {
        this.setState({ screenWidth: window.innerWidth });
        console.log(`width is ${this.state.screenWidth}`);
    }

    render() {        
        return (
            <HashRouter>
                <div>
                <Favicon url='./favicon.ico' />
                    {/* Navigation */}
                    <nav className='navbar fixed-top bg-dark flex-md-nowrap'>
                            {/* Social Media */}
                            <a className='social-media-link' href={socialMediaLinks['instagram']}><FontAwesomeIcon icon={faInstagram} size='lg' /></a>
                            <a className='social-media-link' href={socialMediaLinks['github']}><FontAwesomeIcon icon={faGithub} size='lg' /></a>
                            <a className='social-media-link' href={socialMediaLinks['facebook']}><FontAwesomeIcon icon={faFacebook} size='lg' /></a>
                            <ul className="header">
                                <li className='nav-option'><NavLink exact to='/'>Home</NavLink></li>
                                <li className='nav-option'><NavLink to='/contact'>About/Contact</NavLink></li>
                            </ul>
                    </nav>
                    {/* Main Page */}
                    <div className='content container-fluid' align='center'>
                        <div className='row'>
                            <Route exact path='/' component={withRouter(Home)} />
                            <Route path='/contact' component={withRouter(Contact)} />
                        </div>
                        <footer>Created by me :)</footer>
                    </div>
                </div>
            </HashRouter>
        );
    }
  }

export default Main;

Home.js:

import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

var imageList = [];

function importAll(r) {
    const keys = r.keys();
    let images = {};

    for (var k in keys) {
        images[keys[k].replace('./', '')] = r(keys[k]);
    }

    return images;
}

const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));

for (var image in images) {
    var newImage = new Image(images[image], images[image], null);
    newImage.name = images[image];
    newImage.src = images[image];
    imageList.push(newImage);
}

class Home extends React.Component {
    render() {
        let images = imageList.map(image => {
            if (image.naturalHeight > image.naturalWidth) {         // portrait
                return <img className='portrait-img' src={image.src} alt=''/>
            } else if (image.naturalHeight < image.naturalWidth) {  // landscape
                return <img className='landscape-img' src={image.src} alt=''/>
            }
        });

        return (
            <div>
                {images}
            </div>
        );
    }
}

export default Home;

index.css:

body {
    background-color: #FFF;
    padding: 20px;
    margin: 0;
    font-family: Helvetica;
}

h1 {
    color: #111;
    font-weight: bold;
    padding-top: 2px;
}

ul.header li {
    display: inline;
    list-style-type: none;
    margin: 0;
}

ul.header {
    background-color: #111;
    padding: 0;
}

ul.header li a {
    color: #FFF;
    font-weight: bold;
    text-decoration: none;
    padding: 20px;
    display: inline-block;
}

.content {
    background-color: #FFF;
    padding: 20px;
}

.content h2 {
    padding: 0;
    margin: 0;
}

.content li {
    margin-bottom: 10px;
}

.active {
      background-color: #0099FF;
}

.portrait-img {
    max-width: 55%;
    height: auto;
    padding: 5px;
}

.landscape-img {
    max-width: 75%;
    height: auto;
    padding: 5px;
}

.navbar {
    max-height: 110px;
    background-color: #FFCC00 !important;
}

.content {
    padding-top: 80px;
}

footer {
    background-color: #FFF;
    font-size: 7pt;
    position: relative;
    margin: auto;
    float: left;
}

.social-media-link {
    color: #111;
}

::-webkit-scrollbar {
    display: none;
}

@media only screen and (max-width: 500px) {
    h1 {
        font-size: 18pt;
    }

    .navbar {
        max-height: 120px;
    }

    .content {
        padding-top: 104px;
    }

    .header {
        font-size: 10pt;
    }

    .portrait-img {
        max-width: 100%;
        height: auto;
        padding: 2px;
    }

    .landscape-img {
        max-width: 100%;
        height: auto;
        padding: 2px;
    }

    footer {
        font-size: 5pt;
    }

    .social-media-link {
        padding: 5px;
    }
}

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
    <link rel='icon' href='../src/favicon.ico' type='image/x-icon' />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import Main from './Main';
import './index.css';

ReactDOM.render(
    <Main />,
    document.getElementById('root'),
);

Edit:

revised Home.js below to use a Promise. The idea here is, a Promise to render the images will alter this.state.images and that value will be used in the HTML in the return of the Component's render(). This does not fix the problem, but, it does result in a list of imgs logged instead of a list of undefined as before.

import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import { resolve } from 'q';

var imageList = [];

function importAll(r) {
    const keys = r.keys();
    let images = {};

    for (var k in keys) {
        images[keys[k].replace('./', '')] = r(keys[k]);
    }

    return images;
}

const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));

for (var image in images) {
    var newImage = new Image(images[image], images[image], null);
    newImage.name = images[image];
    newImage.src = images[image];
    imageList.push(newImage);
}

class Home extends React.Component {
    constructor(props) {
        super();
        this.state = {
            images: null,
        };
    }

    componentDidMount() {
        let promise = this.getImages();
        promise.then(result => {
            let images = result.map(image => {
                if (image.naturalHeight > image.naturalWidth) {         // portrait
                    return <img className='portrait-img' src={image.src} alt='' />
                } else if (image.naturalHeight < image.naturalWidth) {  // landscape
                    return <img className='landscape-img' src={image.src} alt='' />
                }
            });
            this.setState({ images: images });
        }, function(error) {
            this.setState({ images: error });
        });
    }

    getImages() {
        let promise = new Promise((resolve, reject) => {
            let imageList = [];
            const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));
            for (var image in images) {
                var newImage = new Image(images[image], images[image], null);
                newImage.name = images[image];
                newImage.src = images[image];
                imageList.push(newImage);
            }

            if (imageList.length > 0) {
                console.log(imageList);
                resolve(imageList);
            } else {
                console.log('shit');
                reject(Error('oh shit'));
            }
        });

        return promise;
    }

    render() {
        return (
            <div>
                {this.state.images}
            </div>
        );
    }
}

export default Home;

This results in:

after hard reload (ctrl+F5): images = [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined] after refresh (ctrl+R): images = [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] -- all with typeof: Symbol(react.element), type: "img".

Upvotes: 0

Views: 3038

Answers (2)

ChumiestBucket
ChumiestBucket

Reputation: 1066

Revising Home.js in the following way resulted in the images loading fast and without refreshing:

import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

var imageList = [];

function importAll(r) {
    const keys = r.keys();
    let images = {};
    for (var k in keys) {
        images[keys[k].replace('./', '')] = r(keys[k]);
    }
    return images;
}

const images = importAll(require.context('./images/resized/', false, /\.(png|jpe?g|svg)$/));

for (var image in images) {
    var newImage = new Image(images[image], images[image], null);
    newImage.name = images[image];
    newImage.src = images[image];
    imageList.push(newImage);
}

class Home extends React.Component {
    constructor() {
        super();
        this.state = {
            images: [],
        }
    }

    componentDidMount() {
        this.setState({ images: imageList });
    }

    render() {
        let imagesFinal = [];
        for (var image in this.state.images) {
            console.log(this.state.images[image].src);
            if (this.state.images[image].naturalHeight > this.state.images[image].naturalWidth) {}
            imagesFinal.push(<img src={this.state.images[image].src} className='landscape-img' alt=''></img>);
        }

        return <div>{imagesFinal}</div>;
    }
}

export default Home;

Upvotes: 0

Eric Haynes
Eric Haynes

Reputation: 5806

console.log is not guaranteed to be synchronous, so you can't depend on it for order of execution. Some engines are, some aren't.

I wouldn't rely on require.context either, particularly if you're using create-react-app. See: https://github.com/facebook/create-react-app/issues/517

In general, if your project is already set up for ES modules and you find yourself needing require, that's a bit of a code smell IMO. The whole point of ES modules is to make scopes static, not dynamic, so that the compiler can infer what to throw away when minifying. The directory structure doesn't actually exist anymore at runtime; it's all been minified into one big file, either on the fly by the webpack dev server, or as part of the build for production.

Are the files too numerous to import directly into a module? You could have a separate module for the imageList that handles the loading and exports it, then just import and use directly in your component(s):

// imageList.js

import foo from './images/foo.jpg'
import bar from './images/bar.jpg'
import baz from './images/baz.jpg'
import blah from './images/blah.jpg'

export default {
  foo,
  bar,
  baz,
  blah,
}

You can then have a test to make sure none get missed in the future (NOTE, tests run in node directly with the file structure, not the webpack-ed final procuct):

// __tests__/imagesList.test.js

import imageList from '../imageList'
import fs from 'fs'
import path from 'path'

const dir = path.join(__dirname, '..', 'images')

it('has all the images', () => {
  const imageFiles = fs.readdirSync(dir).filter((file) => {
    return path.extname(file).match(/\.(png|jpe?g|svg)$/)
  })

  expect(Object.keys(imageList)).toEqual(imageFiles)
})

If there really are too many, or they will change often, I generally like to automate the code creation on the command line. Below is quick n dirty, but should be close to what you need, depending on paths:

#!/usr/bin/env node

const { readdirSync, writeFileSync } = require('fs')
const { extname, join } = require('path')

const relPath = (...paths) => join(__dirname, ...paths)
const dir = relPath('src', 'images')

const printLoaderFile = () => {
  const imageFiles = readdirSync(dir).filter((file) => {
    return extname(file).match(/\.(png|jpe?g|svg)$/)
  })

  const output = []

  imageFiles.forEach((file, index) => {
    output.push(`import image${index} from '${dir}/${file}'`)
  })

  output.push('\nexport default {')
  imageFiles.forEach((file, index) => {
    output.push(`  '${file}': image${index},`)
  })
  output.push('}\n')

  writeFileSync(relPath('src', 'imagesList.js'), output.join('\n'))
}

printLoaderFile()

Upvotes: 3

Related Questions