Prevent browser from making the same async calls as the server - javascript

I am following this tutorial: https://crypt.codemancers.com/posts/2017-06-03-reactjs-server-side-rendering-with-router-v4-and-redux/ which i think is the 'standard' way of doing server side rendering in react (?).
Basically what happens is i use react router (v4) to make a tree of all the components that are about to get rendered:
const promises = branch.map(({ route }) => {
return route.component.fetchInitialData
? route.component.fetchInitialData(store.dispatch)
: Promise.resolve();
});
Wait for all those promises to resolve and then call renderToString.
In my components i have a static function called fetchInitialData which looks like this:
class Users extends React.Component {
static fetchInitialData(dispatch) {
return dispatch(getUsers());
}
componentDidMount() {
this.props.getUsers();
}
render() {
...
}
}
export default connect((state) => {
return { users: state.users };
}, (dispatch) => {
return bindActionCreators({ getUsers }, dispatch);
})(Users);
And all this works great except that getUsers is called both on the server and the client.
I could of course check if any users are loaded and not call getUsers in componentDidMount but there must be a better, explicit way to not make the async call twice.

After getting more and more familiar with react i feel fairly confident i have a solution.
I pass a browserContext object along all rendered routes, much like staticContext on the server. In the browserContext i set two values; isFirstRender and usingDevServer. isFirstRender is only true while the app is rendered for the first time and usingDevServer is only true when using the webpack-dev-server.
const store = createStore(reducers, initialReduxState, middleware);
The entry file for the browser side:
const browserContext = {
isFirstRender: true,
usingDevServer: !!process.env.USING_DEV_SERVER
};
const BrowserApp = () => {
return (
<Provider store={store}>
<BrowserRouter>
{renderRoutes(routes, { store, browserContext })}
</BrowserRouter>
</Provider>
);
};
hydrate(
<BrowserApp />,
document.getElementById('root')
);
browserContext.isFirstRender = false;
USING_DEV_SERVER is defined in the webpack config file using webpack.DefinePlugin
Then i wrote a HOC component that uses this information to fetch initial data only in situations where it is needed:
function wrapInitialDataComponent(Component) {
class InitialDatacomponent extends React.Component {
componentDidMount() {
const { store, browserContext, match } = this.props;
const fetchRequired = browserContext.usingDevServer || !browserContext.isFirstRender;
if (fetchRequired && Component.fetchInitialData) {
Component.fetchInitialData(store.dispatch, match);
}
}
render() {
return <Component {...this.props} />;
}
}
// Copy any static methods.
hoistNonReactStatics(InitialDatacomponent, Component);
// Set display name for debugging.
InitialDatacomponent.displayName = `InitialDatacomponent(${getDisplayName(Component)})`;
return InitialDatacomponent;
}
And then the last thing to do is wrap any components rendered with react router with this HOC component. I did this by simply iterating over the routes recursively:
function wrapRoutes(routes) {
routes.forEach((route) => {
route.component = wrapInitialDataComponent(route.component);
if (route.routes) {
wrapRoutes(route.routes);
}
});
}
const routes = [ ... ];
wrapRoutes(routes);
And that seems to do the trick :)

Related

Is there a way to use the functions inside a component or shared file in react?

I'm trying to access functions inside a class based component which can be used throughout the project. The reason I'm thinking class based is because these request/ functions require an init() method to be called before accessing such data every time. For example:
SharedSDKFile .js
import Facebook from 'facebook-sdk';
class SharedSDKFile extends Component {
constructor() {
Facebook.init({// init some stuff})
}
async user() {
return Facebook.getUser()
}
render() {
return(// ????????????????)
}
}
// *****************************************************
Dashboard.js
// *****************************************************
import Facebook from '../{path}/SharedSDKFile'
const dashboard = () => {
let [person,setPerson] = useState()
// cool function to get and set Users
let user = getUser();
setPerson(user)
// End of cool function
}
I even tried structuring it with a different approach just exporting functions
SharedSDKFile.js
async init() {
// init stuff
}
export const getUser = async(data) => {
init()
// get user
}
// *****************************************************
Dashboard.js
// *****************************************************
import {getUser}from '../{path}/SharedSDKFile'
const dashboard = () => {
let [person,setPerson] = useState()
// cool function to get and set Users
let user = getUser();
setPerson(user)
// End of cool function
}
While a file that exports your function works, the state disappears on reload/ refresh.
Perhaps there is a better solution to this and I'm overthinking it. I have considered redux or localstate, but I will have several functions inside the sharedSDKFile.js which will require several action and reducers...
I am trying to prevent invoking multiple init() and redundancy if I am to import, for example, the FacebookSDK in every file that needs it.
I like using a context singleton approach to isolate external services that can be used app-wide, which may or may not suit what you want to do. It uses context providers/consumers rather than things like Redux.
This isn't the complete picture but hopefully it provides some idea of how the approach might work for you:
FacebookContext.js (or AWSContext.js, or... any specific service)
import React, { Component, createContext } from "react";
import Facebook from "facebook-sdk";
export const FacebookContext = createContext({});
class FacebookProvider extends Component {
state = {
user: null,
// whatever else you need to expose to calling components, like
// lastLoggedIn: null,
// verified: false,
// ...
};
componentDidMount() {
Facebook.init({
// init some stuff
})
}
setUser = user => {
this.setState({
user
});
}
// whatever other methods/data you want this class to expose
render() {
return (
<FacebookContext.Provider
value={{
user: this.state.user // available with FacebookContext.user
// other state values
//
setUser: this.setUser // available with FacebookContext.setUser
// other class methods
}}
>
{this.props.children}
</FacebookContext.Provider>
);
}
}
export default FacebookProvider;
Add the Provider to your top-line app file, something like this in e.g. App.js:
import FacebookProvider from "/path/to/FacebookContext";
// ...
class App extends React.Component {
render() {
return (
<FacebookProvider>
{yourAppRenderStuff}
</FacebookProvider>
);
}
}
Add the Consumer to any calling components:
import { FacebookContext } from "/path/to/FacebookContext";
class SomethingComponent extends Component {
componentDidMount() {
const { facebookContext } = this.props;
facebookContext.setUser(`some user`);// available in other components
console.log(facebookContext.user);// broadcasted to other components
}
render() {
return (
<></>
);
}
}
const Something = () => (
<FacebookContext.Consumer>
{facebookContext => (
<SomethingComponent facebookContext={facebookContext} />
)}
</FacebookContext.Consumer>
);
export default Something;

(New) React Context from a Nested Component not working

I'm having serious issues with the "new" React Context ( https://reactjs.org/docs/context.html ) to work like I want/expect from the documentation. I'm using React v.16.8.6 (upgrading will probably take ages, it's a big app). I know there is a bit of a mix between old and new stuff but plz don't get stuck on that..
I did it like this to be as flexible as possible but it doesn't work.
The issue is, when it comes to contextAddToCart(..) it only executes the empty function instead of the one I defined in state as the documentation this.addToCart. I have consumers in other places as well. It seems like perhaps it's executing this in the wrong order. Or every time a Compontent imports MinicartContext it's reset to empty fn.. I don't know how to get around this..
I'll just post the relevant code I think will explain it best:
webpack.config.js:
const APP_DIR = path.resolve(__dirname, 'src/');
module.exports = function config(env, argv = {}) {
return {
resolve: {
extensions: ['.js', '.jsx'],
modules: [
path.resolve(__dirname, 'src/'),
'node_modules',
],
alias: {
contexts: path.resolve(__dirname, './src/contexts.js'),
},
contexts.js
import React from 'react';
export const MinicartContext = React.createContext({
addToCart: () => {},
getState: () => {},
});
MinicartContainer.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
MinicartContext,
} from 'contexts';
export default class MinicartContainer extends Component {
constructor(props) {
super(props);
this.addToCart = (product, qty) => {
const { prices } = product;
const { grandTotal, qtyTotal } = this.state;
this.setState({
grandTotal: grandTotal + prices.price,
qtyTotal: qtyTotal + qty,
});
};
this.state = {
grandTotal: -1,
qtyTotal: -1,
currencyCode: '',
addToCart: this.addToCart,
};
}
render() {
const { children } = this.props;
return (
<MinicartContext.Provider value={this.state}>
{children}
</MinicartContext.Provider>
);
}
Header.jsx:
import React, { Component } from 'react';
import {
MinicartContext,
} from 'contexts';
class Header extends Component {
render() {
return (
<div>
<MinicartContainer MinicartContext={MinicartContext}>
<Minicart MinicartContext={MinicartContext} />
</MinicartContainer MinicartContext={MinicartContext}>
{/* stuff */}
<MinicartContainer MinicartContext={MinicartContext}>
<Minicart MinicartContext={MinicartContext} />
</MinicartContainer MinicartContext={MinicartContext}>
</div>
)
}
}
export default Header;
AddToCartButton.jsx
import {
MinicartContext,
} from 'contexts';
export default class AddToCartButton extends Component {
addToCart(e, contextAddToCart) {
e.preventDefault();
const QTY = 1;
const { product, active } = this.props;
// doing stuff ...
contextAddToCart(product, QTY);
}
render() {
return (
<React.Fragment>
<MinicartContext.Consumer>
{({context, addToCart}) => (
<div
onClick={(e) => { this.addToCart(e, addToCart); }}
Seems to me that you don't have fully understand how the context API words.
Here's my HOC implementation of contexts, maybe it can help you to understand better how things work.
export const MinicartContext = React.createContext({}) // Export the Context so we can use the Consumer in class and functional components (above). Don't use the Provider from here.
// Wrap the provider to add some custom values.
export const MinicartProvider = props => {
const addToCart = () => {
//Add a default version here
};
const getState = () => {
//Add a default version here
};
// Get the custom values and override with instance ones.
const value = {addToCart, getState, ...props.value}
return <MinicartContext.Provider value={value}>
{props.children}
</MinicartContext.Provider>
}
Then when using the provider:
const SomeComponent = props => {
const addToCart = () => {
//A custom version used only in this component, that need to override the default one
};
//Use the Wrapper, forget the MinicartContext.Provider
return <MinicartProvider value={{addToCart}}>
/* Stuff */
</MinicartProvider>
}
And when using the consumer you have three options:
Class Components with single context
export default class AddToCartButton extends Component {
static contextType = MinicartContext;
render (){
const {addToCart, getState} = this.context;
return (/*Something*/)
}
}
Class Components with multiple contexts
export default class AddToCartButton extends Component {
render (){
return (
<MinicartContext.Consumer>{value => {
const {addToCart, getState} = value
return (/*Something*/)
}}</MinicartContext.Consumer>
)
}
}
Functional Components
const AddToCartButton = props => {
const {addToCart, getState} = useContext(MinicartContext);
}
You can create the Wrapper Provider as a class component too, and pass the full state as value, but it's unnecessary complexity.
I Recommend you take a look at this guide about contexts, and also, avoid using the same name on the same scope... Your AddToCartButton.jsx file was reeeeally confusing :P
The issue I had was that I was using <MinicartContainer> in multiple places but all should act as one and the same. Changing it so it wrapped all elements made other elements reset their state when the context updated.
So the only solution I found was to make everything static (including state) inside MinicartContainer, and keep track of all the instances and then use forceUpdate() on all (needed) instances. (Since I am never doing this.setState nothing ever updates otherwise)
I though the new React context would be a clean replacement for things like Redux but as it stands today it's more a really vague specification which can replace Redux in a (sometimes) non standard way.
If you can just wrap all child Consumers with a single Provider component without any side-effects then you can make it a more clean implementation. That said I don't think what I have done is bad in any way but not what people expect a clean implementation should look like. Also this approach isn't mentioned in the docs at all either.
In addition to Toug's answer, I would memoize the exposed value prop of the provider. Otherwise it will re-render it's subscribers every time even if the state doesn't change.
export const MinicartContext = React.createContext({}) // Export the Context so we can use the Consumer in class and functional components (above). Don't use the Provider from here.
// Wrap the provider to add some custom values.
export const MinicartProvider = props => {
const addToCart = () => {
//Add a default version here
};
const getState = () => {
//Add a default version here
};
// Get the custom values and override with instance ones.
const value = useMemo(
() => ({addToCart, getState, ...props.value}),
[addToCart, getState, props.value]
);
return <MinicartContext.Provider value={value}>
{props.children}
</MinicartContext.Provider>
}

Fetch data only once per React component

I have a simple component that fetches data and only then displays it:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false
stuff: null
};
}
componentDidMount() {
// load stuff
fetch( { path: '/load/stuff' } ).then( stuff => {
this.setState({
loaded: true,
stuff: stuff
});
} );
}
render() {
if ( !this.state.loaded ) {
// not loaded yet
return false;
}
// display component based on loaded stuff
return (
<SomeControl>
{ this.state.stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
);
}
}
Each instance of MyComponent loads the same data from the same URL and I need to somehow store it to avoid duplicate requests to the server.
For example, if I have 10 MyComponent on page - there should be just one request (1 fetch).
My question is what's the correct way to store such data? Should I use static variable? Or I need to use two different components?
Thanks for advice!
For people trying to figure it out using functional component.
If you only want to fetch the data on mount then you can add an empty array as attribute to useEffect
So it would be :
useEffect( () => { yourFetch and set }, []) //Empty array for deps.
You should rather consider using state management library like redux, where you can store all the application state and the components who need data can subscribe to. You can call fetch just one time maybe in the root component of the app and all 10 instances of your component can subscribe to state.
If you want to avoid using redux or some kind of state management library, you can import a file which does the fetching for you. Something along these lines. Essentially the cache is stored within the fetcher.js file. When you import the file, it's not actually imported as separate code every time, so the cache variable is consistent between imports. On the first request, the cache is set to the Promise; on followup requests the Promise is just returned.
// fetcher.js
let cache = null;
export default function makeRequest() {
if (!cache) {
cache = fetch({
path: '/load/stuff'
});
}
return cache;
}
// index.js
import fetcher from './fetcher.js';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false
stuff: null
};
}
componentDidMount() {
// load stuff
fetcher().then( stuff => {
this.setState({
loaded: true,
stuff: stuff
});
} );
}
render() {
if ( !this.state.loaded ) {
// not loaded yet
return false;
}
// display component based on loaded stuff
return (
<SomeControl>
{ this.state.stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
);
}
}
You can use something like the following code to join active requests into one promise:
const f = (cache) => (o) => {
const cached = cache.get(o.path);
if (cached) {
return cached;
}
const p = fetch(o.path).then((result) => {
cache.delete(o.path);
return result;
});
cache.set(o.path, p);
return p;
};
export default f(new Map());//use Map as caching
If you want to simulate the single fetch call with using react only. Then You can use Provider Consumer API from react context API. There you can make only one api call in provider and can use the data in your components.
const YourContext = React.createContext({});//instead of blacnk object you can have array also depending on your data type of response
const { Provider, Consumer } = YourContext
class ProviderComponent extends React.Component {
componentDidMount() {
//make your api call here and and set the value in state
fetch("your/url").then((res) => {
this.setState({
value: res,
})
})
}
render() {
<Provider value={this.state.value}>
{this.props.children}
</Provider>
}
}
export {
Provider,
Consumer,
}
At some top level you can wrap your Page component inside Provider. Like this
<Provider>
<YourParentComponent />
</Provider>
In your components where you want to use your data. You can something like this kind of setup
import { Consumer } from "path to the file having definition of provider and consumer"
<Consumer>
{stuff => <SomeControl>
{ stuff.map( ( item, index ) =>
<h1>items with stuff</h1>
) }
</SomeControl>
}
</Consumer>
The more convenient way is to use some kind of state manager like redux or mobx. You can explore those options also. You can read about Contexts here
link to context react website
Note: This is psuedo code. for exact implementation , refer the link
mentioned above
If your use case suggests that you may have 10 of these components on the page, then I think your second option is the answer - two components. One component for fetching data and rendering children based on the data, and the second component to receive data and render it.
This is the basis for “smart” and “dumb” components. Smart components know how to fetch data and perform operations with those data, while dumb components simply render data given to them. It seems to me that the component you’ve specified above is too smart for its own good.

React fetch data HoC that depends on router params

I have a HoC that fetches data and either returns a loading screen or the underlying component with the data injected.
Now the problem is that the data being fetched depends on a) current URL and b) URL params. I'm using React Router v4. So what I've done is basically put a lot of switch cases in that component. Which works and does what I want it to do, but I'd rather not have the switch cases in this HoC.
const fetchesData = (WrappedComponent) => {
class FetchesData extends React.Component {
constructor(props) {
super(props);
this.fetchData = this.fetchData.bind(this);
this.state = {isLoading: true};
}
fetchData() {
this.setState({isLoading: true});
const {match, dispatch} = this.props;
const {params} = match;
let action = () => {};
switch (match.path) {
case '/': {
action = () => dispatch(
fetchPopularArticles()
);
break;
}
case '/artists/:slug': {
action = () => dispatch(
fetchArtistWithArticles(params.slug)
);
break;
}
// ... more
}
action()
.then((res) => {
this.setState({
...this.state,
isLoading: false,
});
});
}
componentDidMount() {
this.fetchData();
}
render() {
return (
!!this.state.isLoading ?
<LoadingComponent/> :
<WrappedComponent
{...this.props}
/>
);
}
}
return withRouter(connect()(FetchesData));
};
I'd prefer to somehow inject the fetchData() function from the underlying component. Or maybe from parent (router) component.
The first I'm not sure if possible since it would have to mount the underlying component first, which brings more trouble than anything else.
And the former I'm not sure how I would go about doing either since I would need to know the params of the route.
My route rendering looks something like this:
[
<Route
exact={true}
key={0}
path={'/'}
render={(props) => (
<fetchesData(Home)
{...props}/>
)}/>,
// ... more routes
]
What's a good practice for this?
If it helps here's the source:
HoC
Route rendering
Route definitions
As in react data flows down the preferred way would be to pass fetchData method from Render component. You could pass the method to fetchesData like this
const FetchedHome = fetchesData(Home, fetchPopularArticles)
const FetchedArtists = fetchesData(Home, fetchArtistWithArticles)
// ....
<Route
exact={true}
key={0}
path={'/'}
render={(props) => (
< FetchedHome {...props}/>
)}/>
And then inside fetchesData call the passed method
const fetchesData = (WrappedComponent, fetchMethod){
//....
componentDidMount() {
const {match, dispatch} = this.props
dispatch(fetchMethod(match.params));
}
}
Change the action to accept an object
const fetchArtistWithArticles = ({slug: artistSlug})
If you don't want to change your actions you could pass a mapping object from match.params to function attributes you want to send.

Can't unmount React component, returning false from parent component

I'm trying to perform authorization on a child component against certain permissions. I'm using ref callback to get access to the Node, wherein I can check permissions. Depending on these permissions, I would like to unmount the component.
Within the callback, I'm trying to use ReactDOM.findDOMNode() and then ReactDOM.unmountComponentAtNode() to remove it. The latter keeps returning false, although findDomNode appears to properly be selecting the DOM element.
class Auth extends React.Component {
...
checkPermissions(component) {
const domNode = ReactDOM.findDOMNode(component); // => <p>...</p>
if (domNode) {
let wasUnmounted = ReactDOM.unmountComponentAtNode(domNode);
console.log('was unmounted', wasUnmounted); // => false
}
}
render(){
return (
<div>
{this.state.authorized &&
<Component ref={(c) => this.checkPermissions(c)} {...this.props} />
}
</div>
)
}
...
How can I use ReactDOM.unmountComponentAtNode() to effectively remove my component?
I don't think you'll want to mount your node just to check permissions and then unmount it. You should check permissions before you render. Not only is it more secure, but it's also simpler.
If the user is authorized, you render the component. If the user is not authorized, you render something else.
So something kind of like this:
render() {
if (!this.state.authorized) {
return <PleaseLogIn />;
}
return (
<div>
<Component {...this.props} />
</div>
);
}
If you find yourself manipulating the DOM manually, take a step back and make sure there's not a more "Reacty" way to do it.
Update:
If you want a wrapper component that you can put around things that should or shouldn't render its children based on permissions maybe you do something like this:
// state.userPermissions = ['permission1', 'permission1', 'betaTolerant'];
const AuthWrapper = React.createClass({
propTypes: {
userPermissions: React.PropTypes.array.isRequired,
requiredPermissions: React.PropTypes.array.isRequired,
children: React.PropTypes.node.isRequired
},
isAllowed() {
const { userPermissions, requiredPermissions } = this.props;
return requiredPermissions.some((requiredPermission) => {
return userPermissions.some((userPermission) => {
// If this ever returns true, isAllowed will return true
// Meaning: If any of the requiredPermissions match
// any of the userPermissions
return requiredPermission === userPermission;
});
});
},
render {
if(!this.isAllowed()) return null;
return this.props.children;
};
});
const mapStateToProps = (state) => {
// Only this auth component has access to state
return {
userPermissions: state.userPermissions
};
};
export default connect(
mapStateToProps,
null
)(AuthWrapper);
Now you can use this wrapper like:
// Inside some component
render {
return (
<MyApp>
<NormalFeature />
<AuthWrapper requiredPermissions=['secretFeature', 'betaTolerant']>
<SecretFeature />
</AuthWrapper>
</MyApp>
);
}

Categories