React Loadable and Meteor separate bundle - javascript

I am using the following Component with Meteor
https://github.com/CaptainN/npdev-react-loadable
import { Loadable } from 'meteor/npdev:react-loadable';
I create my Loadable component as follows
const HomePageBlog = Loadable({
loading: () => <FullPageLoader />,
loader: () => import('./HomePageBlog'),
});
I have gone through the SSR setup in the docs and it looks something like this
Server index.js
import React from 'react';
import { renderToString, renderToNodeStream } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';
import { StaticRouter } from 'react-router';
import { Helmet } from 'react-helmet';
import Loadable from 'react-loadable';
import { ServerStyleSheet } from 'styled-components';
import {
LoadableCaptureProvider,
preloadAllLoadables,
} from 'meteor/npdev:react-loadable';
preloadAllLoadables().then(() => {
onPageLoad(async (sink) => {
const context = {};
const sheet = new ServerStyleSheet();
const loadableHandle = {};
const routes = (await import('../both/routes.js')).default;
const App = (props) => (
<StaticRouter location={props.location} context={context}>
{routes}
</StaticRouter>
);
const modules = [];
// const html = renderToNodeStream((
const html = renderToString(
<LoadableCaptureProvider handle={loadableHandle}>
<App location={sink.request.url} />
</LoadableCaptureProvider>,
);
// we have a list of modules here, hopefully Meteor will allow to add them to bundle
// console.log(modules);
sink.renderIntoElementById('app', html);
sink.appendToBody(loadableHandle.toScriptTag());
const helmet = Helmet.renderStatic();
// console.log(helmet);
sink.appendToHead(helmet.meta.toString());
sink.appendToHead(helmet.title.toString());
sink.appendToHead(helmet.link.toString());
sink.appendToHead(sheet.getStyleTags());
});
});
client index.js
import { Meteor } from 'meteor/meteor';
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, withRouter } from 'react-router-dom';
import { onPageLoad } from 'meteor/server-render';
import { createBrowserHistory } from 'history';
import { preloadLoadables } from 'meteor/npdev:react-loadable';
console.log('hi');
const history = createBrowserHistory();
/**
* If browser back button was used, flush cache
* This ensures that user will always see an accurate, up-to-date view based on their state
* https://stackoverflow.com/questions/8788802/prevent-safari-loading-from-cache-when-back-button-is-clicked
*/
(function () {
window.onpageshow = function (event) {
if (event.persisted) {
window.location.reload();
}
};
})();
onPageLoad(async () => {
const routes = (await import('../both/routes.js')).default;
const App = () => (
<>
<Router history={history}>
<div>{routes}</div>
</Router>
</>
);
preloadLoadables().then(() => {
ReactDOM.hydrate(<App />, document.getElementById('app'));
});
});
What I am trying to determine is what exactly react loadable does. I am wanting to separate my bundle so I can only load code via SSR when it is needed. Right now I have quite a low score on lighthouse for page speed.
The code that I have here works.
But what I expected to happen was have a separate request to grab more js for the loadable component when it is requested. So it's not in the initial bundle. Is this not how this package works.
Could someone one help me me understand this better.
Thanks for any help ahead of time

Related

No QueryClient set, use QueryClientProvider to set one

I was trying to react query for the first time then I got this at the start of my React app.
import React from 'react'
import { useQuery } from "react-query";
const fetchPanets = async () => {
const result = await fetch('https://swapi.dev/api/people')
return result.json()
}
const Planets = () => {
const { data, status } = useQuery('Planets', fetchPanets)
console.log("data", data, "status", status)
return (
<div>
<h2>Planets</h2>
</div>
)
}
export default Planets
As the error suggests, you need to wrap your application in a QueryClientProvider. This is on the first page of the docs:
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
While this is most commonly caused by not having your application wrapped in a <QueryClientProvider>, in my case it happened because I was importing some shared components, which ended up with a different context. You can fix this by setting the contextSharing option to true
That would look like:
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient} contextSharing={true}>...</QueryClientProvider>
}
From the docs: (https://react-query.tanstack.com/reference/QueryClientProvider)
contextSharing: boolean (defaults to false)
Set this to true to enable context sharing, which will share the first and at least one instance of the context across the window to ensure that if React Query is used across different bundles or microfrontends they will all use the same instance of context, regardless of module scoping.
Just make changes like below it will work fine
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.getElementById('root')
);
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
const queryClient = new QueryClient();
const fetchPanets = async () => {
const result = await fetch('https://swapi.dev/api/people')
return result.json()
}
const Planets = () => {
const { data, status } = useQuery('Planets', fetchPanets)
console.log("data", data, "status", status)
return (
<div>
<h2>Planets</h2>
</div>
);
}
export default function Wraped(){
return(<QueryClientProvider client={queryClient}>
<Planets/>
</QueryClientProvider>
);
}
Single SPA (micro-frontend) - React Query v3.34.15
I was getting this error while trying to integrate a sigle-spa react parcel into the root application.
I used craco-plugin-single-spa-application for the building of a CRA app as a way to adapt it for a parcel. In the entry config I was pointing to my single-spa-react config.
// craco.config.js
const singleSpaApplicationPlugin = require('craco-plugin-single-spa-application')
module.exports = {
plugins: [
{
plugin: singleSpaApplicationPlugin,
options: {
orgName: 'uh-platform',
projectName: 'hosting',
entry: 'src/config/single-spa-index.cf.js',
orgPackagesAsExternal: false,
reactPackagesAsExternal: true,
externals: [],
minimize: false
}
}
]
}
In the single-spa-index.cf.js file I had the following configs.
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import App from '../App'
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary() {
return <div>Ocorreu um erro desconhecido!</div>
}
})
export const { bootstrap, mount, unmount } = lifecycles
After reading a bunch of forums and the react-query documentation, the only thing that I figured out I needed to change was pass in the QueryClientProvider the prop contextSharing as true. After had did this change, ran the building and access the route that opens my parcel. I got the same error.
import React from 'react'
import ReactDOM from 'react-dom'
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import App from './App'
const queryClient = new QueryClient()
const isDevelopmentEnv = process.env.NODE_ENV === 'development'
if (isDevelopmentEnv) {
import('./config/msw/worker').then(({ worker }) => worker.start())
}
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider contextSharing={true} client={queryClient}>
<App />
{isDevelopmentEnv && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root')
)
But, how do I solved that. Well, it was was simple. I couldn't even imagine why it was working locally. But not after building and integration.
The problem was because I put the React Query Provider inside the index o the application and in my single-spa-index.cf.js I was importing import App from '../App' which really wasn't wrapped by the provider. Once I also was importing App in the application index, where It was wrapped making It works locally. 😢😢
So after figure that out, my code was like that:
CODE AFTER SOLUTION
// craco.config.js
const singleSpaApplicationPlugin = require('craco-plugin-single-spa-application')
module.exports = {
plugins: [
{
plugin: singleSpaApplicationPlugin,
options: {
orgName: 'uh-platform',
projectName: 'hosting',
entry: 'src/config/single-spa-index.cf.js',
orgPackagesAsExternal: false,
reactPackagesAsExternal: true,
externals: [],
minimize: false
}
}
]
}
// src/config/single-spa-index.cf.js
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import App from '../App'
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary() {
return <div>Ocorreu um erro desconhecido!</div>
}
})
export const { bootstrap, mount, unmount } = lifecycles
// App.tsx
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import { config } from 'config/react-query'
import Routes from 'routes'
import GlobalStyles from 'styles/global'
import * as S from './styles/shared'
const queryClient = new QueryClient(config)
const isDevelopmentEnv = process.env.NODE_ENV === 'development'
if (isDevelopmentEnv) {
import('./config/msw/worker').then(({ worker }) => worker.start())
}
function App() {
return (
<QueryClientProvider contextSharing={true} client={queryClient}>
<S.PanelWrapper>
<Routes />
<GlobalStyles />
</S.PanelWrapper>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
export default App
// index.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('root')
)
Well, it was long but I hope it helps someone that's undergoing for the same problem as mine. 🙌🙌🙌
I was trying to fix the same thing:
I followed the React Query docs
and used the concept of Higher Order Component
See if it helps:
import React from 'react';
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import Planet from './Planet';
const queryClient = new QueryClient();
const fetchPlanets = async () => {
const res = await fetch('http://swapi.dev/api/planets/');
return res.json();
}
const Planets = () => {
const { data, status } = useQuery('planets', fetchPlanets);
return (
<div>
<h2>Planets</h2>
{ status === 'loading' && (<div>Loading data...</div>)}
{ status === 'error' && (<div>Error fetching data</div>)}
{
status === 'success' && (
data.results.map(planet =>
<Planet
key={planet.name}
planet={planet}
/>
)
)
}
</div>
)
}
// Higher order function
const hof = (WrappedComponent) => {
// Its job is to return a react component warpping the baby component
return (props) => (
<QueryClientProvider client={queryClient}>
<WrappedComponent {...props} />
</QueryClientProvider>
);
};
export default hof(Planets);
In my case I was importtng from 'react-query' in one place and '#tanstack/react-query' in another.
I got that error when trying to add the react-query devtools.
The problem was I was installing it wrongly according my version, I was using react-query v3.
WRONG FOR react-query V3 (GOOD FOR V4)
import { ReactQueryDevtools } from '#tanstack/react-query-devtools';
OK FOR react-query V3
import { ReactQueryDevtools } from 'react-query/devtools';
In my case I accidentally used two different versions of react-query in my modules.
In my case
Error import { QueryClient, QueryClientProvider } from "#tanstack/react-query";
Solution import { QueryClient, QueryClientProvider } from "react-query";
remove it #tanstack/
Just be careful when upgrade from react-query v3 to #tanstack/react-query v4.
Ensure that you replace all imports as "react-query" to "#tanstack/react-query" and then run yarn remove the lib that you won't use anymore, otherwise you may accidentally import the unexpected one.
This happened to me and caused this error.

SSR app but React Router Navigation on the client side doesn't fetch data and breaks the page

I've created a SSR React app that loads data on the server and sends it to the client as html. The problem kicks in after the initial server request has been served and i try to switch to a different navigation link. The url changes to the correct path but the page itself breaks with a TypeError: Cannot read property 'length' or 'map' of undefined. I believe the fetching somehow is not working on the client side, because if i turn off JavaScript from the browser everything works just fine.
The App has four routes, Home, Movies (needs to fetch data), TvShows (needs to fetch data) and PageNotFound. Again, the problem occurs when for example I open the Home page and try to switch to Movies. However, if i open Movies or TvShows first everything loads correctly because of the initial request being served by the server. Here is my file structure:
And here's the content of some of my files:
index.js (Server.js)
import "#babel/polyfill";
import express from "express";
import { applyMiddleware, createStore } from "redux";
import Routes from "./client/Routes";
import { matchRoutes } from "react-router-config";
import renderer from "./helpers/renderer.js";
import thunk from "redux-thunk";
import reducers from "./reducers";
const compression = require("compression");
const app = express();
app.use(compression());
app.use(express.static("public")); //treats the public(client side) directory as public, available to the outside world
// This is fired every time the server side receives a request
app.get("*", (req, res) => {
// Create a new Redux store instance
const store = createStore(reducers, {}, applyMiddleware(thunk));
const promises = matchRoutes(Routes, req.path)
.map(({ route }) => {
return route.loadData ? route.loadData(store) : null;
})
.map((promise) => {
if (promise) {
return new Promise((resolve, reject) => {
promise.then(resolve).catch(resolve);
});
}
});
Promise.all(promises).then(() => {
// Send the rendered page back to the client
// Grab the initial state from our Redux store
const context = {};
//const finalState = store.getState();
const content = renderer(req, store, context);
if (context.notFound) {
res.status(404);
}
res.send(content);
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT);
client.js
import "#babel/polyfill";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { Provider } from "react-redux";
import { renderRoutes } from "react-router-config";
import Routes from "./Routes";
import reducers from "../reducers";
// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__;
// Create Redux store with initial state
const store = createStore(reducers, preloadedState, applyMiddleware(thunk));
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<div>{renderRoutes(Routes)}</div>
</BrowserRouter>
</Provider>,
document.querySelector("#root")
);
renderer.js
import React from "react";
import serialize from "serialize-javascript";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { Provider } from "react-redux";
import Routes from "../client/Routes";
import { renderRoutes } from "react-router-config";
import { Helmet } from "react-helmet";
// Render the component to a string
export default (req, store, context) => {
const html = renderToString(
<Provider store={store}>
<StaticRouter location={req.path} context={context}>
<div>{renderRoutes(Routes)}</div>
</StaticRouter>
</Provider>
);
const helmet = Helmet.renderStatic();
return `
<!doctype html>
<html>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap#4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div id="root">${html}</div>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// https://redux.js.org/recipes/server-rendering/#security-considerations
window.__PRELOADED_STATE__ = ${serialize(store.getState())}
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
};
Routes.js
import App from "./App";
import HomePage from "./pages/HomePage";
import MovieListPage from "./pages/MovieListPage";
import TvShowsPage from "./pages/TvShowsPage";
import NotFoundPage from "./pages/NotFoundPage";
//using spread operator for the components
//and loadData function(if available)
//because they are imported in object form now
export default [
{
...App, //no path added to App, meaning it will always be displayed on screen
routes: [
{
...HomePage,
path: "/",
exact: true,
},
{
...MovieListPage,
path: "/movies",
exact: true,
},
{
...TvShowsPage,
path: "/tvshows",
exact: true,
},
{
...NotFoundPage, //will be shown if react router can't match any of the defined routes
},
],
},
];
MovieListPage.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchMovies } from "../../actions";
import { Helmet } from "react-helmet";
class MovieListPage extends Component {
// Have state ready for both Movies and TvShows link clicks/direct requests
componentDidMount() {
if (this.props.movies) return;
this.props.fetchMovies();
}
renderData() {
return this.props.movies.results.map((movie) => {
return (
<div
key={movie.id}
className="card text-center m-3"
style={{ width: "15rem" }}
>
<img
className="card-img-top"
alt="..."
src={this.dynamicUrl(movie)}
/>
<div className="card-body">
<h5 className="card-title">{movie.title}</h5>
<p className="card-text font-weight-light">{movie.release_date}</p>
<a href={this.dynamicLink(movie)} className="btn btn-secondary">
TMDB
</a>
</div>
</div>
);
});
}
dynamicUrl(movie) {
let url = "https://image.tmdb.org/t/p/w200/" + movie.poster_path;
return url;
}
dynamicLink(movie) {
let link = "https://www.themoviedb.org/movie/" + movie.id;
return link;
}
head() {
return (
<Helmet>
<title>{`${this.props.movies.results.length} Movies Loaded`}</title>
<meta property="og:title" content="Movies" />
</Helmet>
);
}
render() {
return (
<div className="container">
{this.head()}
<div className="row">{this.renderData()}</div>
</div>
);
}
}
function mapStateToProps(state) {
return { movies: state.movies };
}
function loadData(store) {
return store.dispatch(fetchMovies());
}
//exporting the component and the loadData function (if present)
//in the form of an object(key:value pair)
// to avoid overlap of different loadData function imports in Routes
export default {
loadData,
component: connect(mapStateToProps, { fetchMovies })(MovieListPage),
};
I can't seem to figure out what is it that i'm missing.
I found the issue in renderer.js. I didn't provide the correct path for my client-side bundle.js in the html served by the server. Instead of <script src="/bundle.js"></script> it had to be <script src="/public/bundle.js"></script>.

React native - Data fetch from Async storage before rendering the app component

I am creating an iOS app on React Native and want to support multi languages for the app.
The application works when set the target language, but when I open the app again it shows the default language. I am trying to get the defined language from async storage but it does not reflect the changes and render the app component. I ma using context api for localization.
I use the Launchscreen.xib for showing the splash screen and hide this splash screen using react-native-splash-screen method SplashScreen.hide() in app.js useEffect hook and I want to set the previously chosen language before loading the app component.
Localization.js file -
import React, {createContext, useState} from 'react';
import * as RNLocalize from 'react-native-localize';
import LocalizedStrings from 'react-native-localization';
import AsyncStorage from '#react-native-community/async-storage';
import en from './translations/en.json';
import de from './translations/de.json';
import bg from './translations/bg.json';
import it from './translations/it.json';
import nl from './translations/nl.json';
import pl from './translations/pl.json';
import pt from './translations/pt.json';
import ru from './translations/ru.json';
import fr from './translations/fr.json';
import es from './translations/es.json';
const APP_LANGUAGE = 'appLanguage';
const DEFAULT_LANGUAGE = 'en';
const languages = {en, de, bg, it, nl, pl, pt, ru, fr, es};
const translations = new LocalizedStrings(languages);
export const LocalizationContext = createContext({
translations,
setAppLanguage: () => {},
appLanguage: DEFAULT_LANGUAGE,
initializeAppLanguage: () => {},
});
export const LocalizationProvider = ({children}) => {
const [appLanguage, setAppLanguage] = useState(DEFAULT_LANGUAGE);
//console.log(appLanguage);
const setLanguage = language => {
translations.setLanguage(language);
setAppLanguage(language);
AsyncStorage.setItem(APP_LANGUAGE, language);
};
const initializeAppLanguage = async () => {
const currentLanguage = await AsyncStorage.getItem(APP_LANGUAGE);
console.log('language' , currentLanguage);
if (currentLanguage === null) {
let localeCode = DEFAULT_LANGUAGE;
const supportedLocaleCodes = translations.getAvailableLanguages();
const phoneLocaleCodes = RNLocalize.getLocales().map(
locale => locale.languageCode,
);
phoneLocaleCodes.some(code => {
if (supportedLocaleCodes.includes(code)) {
localeCode = code;
return true;
}
});
setLanguage(localeCode);
} else {
setLanguage(currentLanguage);
}
onSuccess();
};
return (
<LocalizationContext.Provider
value={{
translations,
setAppLanguage: setLanguage,
appLanguage,
initializeAppLanguage,
}}>
{children}
</LocalizationContext.Provider>
);
};
App.js file
import React, {useEffect, useContext, useState} from 'react';
import {createStore, combineReducers, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import {Provider} from 'react-redux';
import logger from 'redux-logger';
import ReduxThunk from 'redux-thunk';
import loginReducer from '_store/reducers/login.js';
import transportReducer from '_store/reducers/transport.js';
import gpsReducer from '_store/reducers/gpslogger.js';
import AppNavigator from '_navigations/AppNavigator';
import SplashScreen from 'react-native-splash-screen';
import FlashMessage from 'react-native-flash-message';
import {LocalizationContext} from './Localization';
const rootReducer = combineReducers({
auth: loginReducer,
transport: transportReducer,
gps: gpsReducer
});
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(logger, ReduxThunk)));
const App = props => {
const {initializeAppLanguage, appLanguage} = useContext(LocalizationContext);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
initializeAppLanguage();
SplashScreen.hide();
}, []);
return (
<Provider store={store}>
<AppNavigator />
<FlashMessage />
</Provider>
);
};
export default App;
How can I achieve the required behaviour?
I solved it by creating a separate splash screen and putting a setTimeout method there. it gives time to fetch the data from async storage and then navigate to main app.
We are doing sth that achieve the same wanted behavior in app.js
setI18nConfig = () => {
const {dispatch} = this.store;
const translationGetters = {
ar: () => require("../src/assets/translations/ar.json"),
en: () => require("../src/assets/translations/en.json"),
};
const translate = helpers.translate();
const fallback = { languageTag: "en", isRTL: false };
const { languageTag, isRTL } = this.appLanguage ? this.appLanguage : (RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) || fallback)
dispatch(setAppLanguage({
languageTag,
isRTL
}));
if(!this.appLanguage) {
this.appLanguage = {languageTag, isRTL}
}
// clear translation cache
translate.cache.clear();
// set i18n-js config
i18n.translations = { [languageTag]: translationGetters[languageTag]() };
i18n.locale = languageTag;
}

React not rendering on desktop safari or any mobile (iOS tested only) browser when page refresh or manual navigation

This seems like a duplicate of a few others. But no solution has worked for me.
Navigating via links works fine. Refreshing pages or manual navigating only works on desktop (chrome and firefox, not working on Safari).
On desktop safari, and all iOS browsers, it simply shows the entire JSON object in the browser and doesn't seem to be serving the static files.
I’ve tried Router, BrowserRouter and HashRouter. The only one that works is HashRouter. But, I don’t want hashes in the url.
I'm not getting any errors, and I've console logged all over.
When I placed a log in the getProducts action creator and on the server "/products" route, Safari doesn't show the action creator console log in the browser. But, heroku logs show that the path="/products" is being hit, but not the path="/static/css/main.etc.," or path="/static/js/main.etc.," files.
Things I've looked into and/or tried:
React-router urls don't work when refreshing or writting manually
Web page not rendering correctly in iOS browsers or desktop Safari
https://github.com/ReactTraining/react-router/issues/4727
How to remove the hash from the url in react-router
React Routing works in local machine but not Heroku
https://github.com/ReactTraining/react-router/issues/4671
Here's a stripped back sample. Note: I'm using concurrently to proxy my requests.
// client/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './styles/index.css';
import App from './App'
import registerServiceWorker from './registerServiceWorker'
import { Router } from 'react-router-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import reduxThunk from 'redux-thunk'
import history from './history'
import reducers from './reducers'
const store = createStore(
reducers,
applyMiddleware(reduxThunk)
)
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>
, document.getElementById('root'))
registerServiceWorker();
// client/history.js
import createHistory from 'history/createBrowserHistory'
export default createHistory()
// client/App.js
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom'
import Home from './components/home'
import Header from './components/header'
import Products from './components/products'
import './styles/App.css'
class App extends Component {
render() {
return (
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/products" component={Products} />
<Route render={() => <p>Not found</p>} />
</Switch>
</div>
);
}
}
export default App;
// client/components/products.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as actions from '../actions'
// import ‘../polyfill’ // imported polyfil object from core-js when previously using Object.values below… same results either way…
class Products extends Component {
componentWillMount() {
this.props.getProducts()
}
renderProducts() {
/* const { products } = this.props
return Object.values(products).map((product) => {
return (
<li key={product.title}>
{product.title}
</li>
)
});*/
const productsArray = []
const { products } = this.props
for(let key in products) {
productsArray.push(<li key={products[key].title} >{products[key].title}</li>)
}
return productsArray
}
render() {
if(!this.props.products) {
return (
<div></div>
)
}
return (
<div>
<ul className="productListItemUl" >{this.renderProducts()}</ul>
</div>
)
}
}
const mapStateToProps = state => {
return { products: state.products.products }
}
export default connect(mapStateToProps, actions)(Products)
// actions/index.js
import axios from 'axios'
import {
GET_PRODUCTS
} from './types'
export function getProducts() {
return async function(dispatch) {
try {
const products = await axios.get('/products')
dispatch({ type: GET_PRODUCTS, payload: products.data })
} catch (err) {
console.log('redux thunk getProducts() action creator error')
console.log(err)
}
}
}
// server.js
"use strict";
require("babel-core")
const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 3050;
const mongoUtil = require('./server/mongoUtil')
mongoUtil.connect()
const bodyParser = require('body-parser');
const jsonParser = bodyParser.json();
app.use(jsonParser);
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.resolve(__dirname, 'client/build')));
}
let productsRoute = require('./server/routes/products');
app.use('/products', productsRoute)
app.get('*', function(request, response) {
response.sendFile(path.resolve(__dirname, 'client/build', 'index.html'));
});
app.listen(port, () => console.log(`Listening on port ${port}.`));

How to do server-side rendering in React/redux?

I am new to react/redux I little confused with server side rending in react/redux,
Yes i saw some example on the internet but when i tried with mock api with external server , server side rendering is not working .
cat.js
import React from 'react';
import {render} from 'react-dom';
import {connect} from 'react-redux';
import * as mockApi from '../Actions/mockActions';
class Cat extends React.Component{
componentWillMount(){
this.props.getMockApi();
}
render(){
return(
<div>
Hello Dude
{this.props.mock.data.map((data,i) => {
return <li key={i}>{data.email}</li>
})}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
mock:state.mock
}
};
const mapDispatchToProps = (dispatch) => {
return {
getMockApi:() => dispatch(mockApi.getMockData())
}
};
export default connect(mapStateToProps,mapDispatchToProps)(Cat);
mockActions.js
import axios from 'axios';
import * as types from './actionTypes';
export function getMockData() {
return dispatch => {
return axios.get('http://jsonplaceholder.typicode.com/users').then(response => {
dispatch(setThisData(response.data))
})
}
}
export function setThisData(data) {
return {
type:types.MOCK_API,
payload:data
}
}
App.js
import React from 'react';
import {render} from 'react-dom';
import Cat from './components/cat'
import {Provider} from 'react-redux';
import configureStore from './Store/configureStore';
import { createStore ,applyMiddleware,compose} from 'redux';
import counterApp from './Reducers'
import thunk from 'redux-thunk';
if(typeof window !== 'undefined'){
// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__
// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__
const store = createStore(counterApp, preloadedState, compose(applyMiddleware(thunk)))
render(
<Provider store={store} >
<Cat/>
</Provider>
,
document.getElementById('app')
)
}
devServer.js
import express from 'express';
import path from 'path';
import webpack from 'webpack';
import webpackMiddleware from 'webpack-dev-middleware'
import webpackHotMidleware from 'webpack-hot-middleware';
import bodyParser from 'body-parser';
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux';
import counterApp from '../../src/client/ReduxServer/Reducers';
import App from '../../src/client/ReduxServer/components/cat';
import { renderToString } from 'react-dom/server'
import webpackConfig from '../../webpack.config.dev';
let app = express();
app.use(bodyParser.json());
app.use(express.static('public'))
const compiler = webpack(webpackConfig);
app.use(webpackMiddleware(compiler, {
hot: true,
publicPath: webpackConfig.output.publicPath,
noInfo: true
}));
app.use(webpackHotMidleware(compiler));
// app.get('/*', (req, res) => {
// res.sendFile(path.join(__dirname, '../../index.html'))
// });
//Redux Start
app.use(handleRender);
function handleRender(req,res) {
const store = createStore(counterApp);
const html = renderToString(
<Provider store={store} >
<App/>
</Provider>
)
const preloadedState = store.getState();
// Send the rendered page back to the client
res.send(renderFullPage(html, preloadedState))
}
function renderFullPage(html, preloadedState) {
console.log(preloadedState)
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="app">${html}</div>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
<script src="bundle.js"></script>
</body>
</html>
`
}
//Redux Ends
app.listen(3000, () => {
console.log('Listening')
});
Right now this will only server render the hello dude but not the mock Api call data .I know that missed to fetch the data from server side but the point is what will i do If ihave to render a two components and that component has 5 api reuqest ,And how to fecth the correct api Request
Right Now My client Side Prefecthed state will look like this
window.__PRELOADED_STATE__ = {"mock":{"data":[]}}
Ok, to make this clear, you've created the code to handle server rendering. However, it doesn't load the data that is supposed to be fetched right?
You've done the first step, great! The next step is to load the actual dynamic data to the store. Let's look at this code here
function handleRender(req,res) {
const store = createStore(counterApp);
const html = renderToString(
<Provider store={store} >
<App/>
</Provider>
)
const preloadedState = store.getState();
// Send the rendered page back to the client
res.send(renderFullPage(html, preloadedState))
}
What happened is that you created a store. The store is used to render the html into a string. Then you get the store state and put it into preloadedState.
This is great accept that renderToString will not call this.props.getMockApi(); as you would expect.
Instead, you have to fetch the state before you call renderToString();
In this case, what you could do is as following. (Note that this is just an example, you probably want to use something more general in production, especially if you use something like react-router.)
import * as mockApi from '../Actions/mockActions';
function handleRender(req, res) {
const store = createStore(counterApp);
store.dispatch(mockApi.getMockData())
// And since you used redux-thunk, it should return a promise
.then(() => {
const html = renderToString(
<Provider store={store}>
<App/>
</Provider>
)
const preloadedState = store.getState();
// Send the rendered page back to the client
res.send(renderFullPage(html, preloadedState))
});
}
Simple isn't it? ;D, nah just joking. This is one part of react where there's not really an exact solution to the problem yet.
Personally, if I had the choice to go back in time, I'd tell myself to learn other stuff other than server rendering. There are other techniques such as code splitting, lazy loading, etc that I could've used instead. With server rendering, if the javascript arrives long after the user has seen the initial page, they might get frustrated by other things that require js. For example in my case, some links are not working, some buttons don't do anything, etc.
I'm not saying that server rendering is not good. It's an interesting technique, just that there are other techniques that are more beneficial to learn first (Oh, and server rendering basically locks you to use nodejs for your backend). Good luck to you :)

Categories