Search code examples
reactjswebpackreact-routerwebpack-3

React Router can't configure URLs on localhost and remote. Receiving 404 not found error


I've encountered weird situation. I believe that the issue is related to React Router V4 configuration.

I'm using the react-router-modal and React Router v4. With react-router-modal component I render a link to a modal window which have it's own unique URL. Once a link to a modal is clicked - the modal is opened and the URL is added to the address bar. So I could even access the modal window from a new tab with this url:http://localhost:8080/modal_1URL what is very crucial for me.

In dev mode (npm start) everything works fine, also once the invalid URL is entered the page just reloads and invalid URL is remained in address bar. (Not best case)

I thought that everything will work as it is in a production build. But here is a problems. Once the final build is uploaded to the remote server or localhost I get these errors:
1. Once the invalid URL is entered - I receive 404 Not Found
2. Once I try to access a modal with a straight URL (not clicked in loaded page) http://localhost:8080/modal_1 - 404 Not Found

No .htaccess is uploaded

The index.js is very simple and the whole application is onepage with various components:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { ModalContainer } from 'react-router-modal';
import LandingPage from './components/villages/Landing Page.js';
import WOW from 'wowjs';

import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';

class App extends React.Component {
  componentDidMount() {
    new WOW.WOW().init();
  }

  render() {
    return (
      <div>
        <LandingPage />        
      </div>
    )
  }
}

ReactDOM.render(
  <I18nextProvider i18n={i18n}>
    <BrowserRouter>
      <div>
        <App />
        <ModalContainer />
      </div>
    </BrowserRouter>
  </I18nextProvider>,
  document.getElementById('app')
);

And the component witch renders the modals looks like:

import React from 'react';
import ReactDOM from 'react-dom';
import { ModalRoute, ModalLink } from 'react-router-modal';
import { Link } from 'react-router-dom';
import ImageLoader from 'react-load-image';

function Preloader(props) {
  return <img src="img/spinner.gif" className="img-responsive center-block image-loader" />;
}

function ModalOne(props) {
  const { t } = props;
  return (
    <div className='basic__modal-content'>
    ...
    </div>
  );
}
const ExtendedModalOne = translate()(ModalOne);


class Items extends React.Component {

  render() {
    const { t } = this.props;
    return (
      <div className="container">
        <ul>
          <div id="works">
            <li>
              <Link to="/modal_1"> <img src="img/" /> </Link>
              <h3>Name</h3>
            </li>
          </div>
        </ul>
        <ModalLink component={ExtendedModalOne} path={`/modal_1`} />
        <ModalLink component={ExtendedModalTwo} path={`/modal_2`} />        
      </div>
    )
  }
}

module.exports = translate()(Items);

and webpack.config:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PreloadWebpackPlugin = require('preload-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const autoprefixer = require('autoprefixer');

const staticSourcePath = path.join(__dirname, 'css');
const sourcePath = path.join(__dirname);
const buildPath = path.join(__dirname, 'dist');

module.exports = {
    stats: {
    warnings: false
},
    devtool: 'cheap-module-source-map',
      devServer: {    
      historyApiFallback: true,
      contentBase: './'
  },
    entry: {
        app: path.resolve(sourcePath, 'index.js')
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].[chunkhash].js',
        publicPath: '/'
    },
    resolve: {
        extensions: ['.webpack-loader.js', '.web-loader.js', '.loader.js', '.js', '.jsx'],
        modules: [
            sourcePath,
            path.resolve(__dirname, 'node_modules')
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')
        }),
        new webpack.optimize.ModuleConcatenationPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: 'vendor.[chunkhash].js',
            minChunks (module) {
                return module.context && module.context.indexOf('node_modules') >= 0;
            }
        }),
        new webpack.LoaderOptionsPlugin({
            options: {
                postcss: [
                    autoprefixer({
                        browsers: [
                            'last 3 version',
                            'ie >= 10'
                        ]
                    })
                ],
                context: staticSourcePath
            }
        }),
        new webpack.HashedModuleIdsPlugin(),
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'index.html'),
            path: buildPath,
            excludeChunks: ['base'],
            filename: 'index.html',
            minify: {
                collapseWhitespace: true,
                collapseInlineTagWhitespace: true,
                removeComments: true,
                removeRedundantAttributes: true
            }
        }),
        new PreloadWebpackPlugin({
            rel: 'preload',
            as: 'script',
            include: 'all',
            fileBlacklist: [/\.(css|map)$/, /base?.+/]
        }),
        new webpack.NoEmitOnErrorsPlugin(),
        new CompressionPlugin({
            asset: '[path].gz[query]',
            algorithm: 'gzip',
            test: /\.js$|\.css$|\.html$|\.eot?.+$|\.ttf?.+$|\.woff?.+$|\.svg?.+$/,
            threshold: 10240,
            minRatio: 0.8
        })
    ],
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                  loader: 'babel-loader',
                  options: {
                    presets: ['env', 'react']
                  }
                },
                include: sourcePath
            },
            {
                test: /\.(eot?.+|svg?.+|ttf?.+|otf?.+|woff?.+|woff2?.+)$/,
                use: 'file-loader?name=assets/[name]-[hash].[ext]'
            },
            {
                test: /\.(png|gif|jpg|svg)$/,
                use: [
                    'url-loader?limit=20480&name=assets/[name]-[hash].[ext]'
                ],
                include: staticSourcePath
            }
        ]
    }
}; 

Solution

  • Your HTTP server should always send index.html file for any route.

    Example NodeJS Express configuration:

    // ...
    const app = express();
    
    app.get('*', (req, res) => {
      res.sendFile('path/to/your/index.html')
    })
    
    // ...
    

    As I know, for the Apache server you can use:

    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.html [L]
    

    It tells to Apache server to rewrite everything to the index.html page file and let the client handle routing.