Monday, 21 September 2020

How do I implement Sibling Component communication in App shell and SSR

I have adopted a project that was built on this starter kit. This architecture employs App Shell and SSR. I am trying to add a simple search bar and this will mean passing the search keys from the search bar component to the post-list component so they can be filtered. I have found that this is nearly impossible with Context Providers and Consumers. I would like to use Context, but I do not know how to do it. It looks like this starter kit has this as a serious shortcoming and if it could be solved, it would make this kit more useful online.

The use of Hydrate seems to preclude the simple application of a context provider. Hydrate adds components in a parallel way with no way to have the Context Provider above both of them. This pattern I am using here does not work. When I update the provider it does not cause a re-render of the context consumer.

If I have to use something other than Context, like say Redux, then I will accept that answer.

Here is the client entry point:

import { onPageLoad } from 'meteor/server-render';
import MeteorLoadable from 'meteor/nemms:meteor-react-loadable';
import { Switch, Route, Router, Redirect } from 'react-router-dom';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import apolloLogger from 'apollo-link-logger';
import { onTokenChange, getLoginToken } from '/app/ui/apollo-client/auth';
import createBrowserHistory from 'history/createBrowserHistory';
import 'unfetch/polyfill';

// Initialise react-intl
import { primaryLocale, otherLocales } from '/app/intl';

// Need to preload of list of loadable components for MeteorLoadable
import '/app/ui/loadables';

// To get started, create an ApolloClient instance and point it at your GraphQL
// server. By default, this client will send queries to the '/graphql' endpoint
// on the same host.

// To avoid asynchronously accessing local storage for every GraphQL request,
// we cache the authorisation token, and update it using an onTokenChange callback
let authToken;
let authTokenInitialised = false;
onTokenChange(({ token }) => { authToken = token; authTokenInitialised = true; });

const withAuthToken = setContext(() => {
  if (authTokenInitialised) {
    return authToken ? { headers: { authorization: authToken } } : undefined;
  }

  return getLoginToken()
    .then((token) => {
      authToken = token;
      authTokenInitialised = true;
      return authToken ? { headers: { authorization: authToken } } : undefined;
    });
});

const resetAuthToken = onError(({ networkError }) => {
  if (networkError && networkError.statusCode === 401) {
    // Remove cached token on 401 from the server
    authToken = null;
    authTokenInitialised = false;
  }
});

const onErrorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.map(({ message, locations, path }) => console.log(
      `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
    ));
  }
  if (networkError) console.log(`[Network error]: ${networkError}`);
});

const client = new ApolloClient({
  link: ApolloLink.from([
    apolloLogger,
    withAuthToken,
    resetAuthToken,
    onErrorLink,
    new HttpLink({
      uri: '/graphql',
    }),
  ]),
  cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
});

// Inject the data into the app shell.
// If the structure's changed, ssr.js also needs updating.
async function renderAsync() {
  const [
    React,
    { hydrate, render },
    { default: App },
    { default: HeaderTitle },
    { default: LanguagePicker },
    { default: Routes },
    { default: Menu },
  ] = await Promise.all([
    import('react'),
    import('react-dom'),
    import('/app/ui/components/smart/app'),
    import('/app/ui/components/smart/header/header-title'),
    import('/app/ui/components/dumb/language-picker'),
    import('/app/ui/routes'),
    import('/app/ui/components/smart/menu'),
    MeteorLoadable.preloadComponents(),
  ]);

  // Given that we are implementing App Shell Architecture and, therefore,
  // injecting (via reactDOM.render) the Header, Menu and Main components into
  // different HTML elements, we need a way to share the router 'history' among
  // all three mentioned components.
  // As a default, for every invocation of 'BrowserRouter', there will be new
  // 'history' instance created. Then, changes in the 'history' object in one
  // component won't be available in the other components. To prevent this, we are
  // relying on the 'Router' component instead of 'BrowserRouter' and defining our
  // custom 'history' object by means of 'createBrowserHistory' function. Said
  // 'history' object is then passed to every invocation of 'Router' and therefore
  // the same 'history' object will be shared among all three mentioned components.

  const history = createBrowserHistory();

  // Inject react app components into App's Shell
  const ClientApp = ({ component }) => (
    <Router history={history}>
      <ApolloProvider client={client}>
        <Switch>
          {/* Map our locales to separate routes */}
          { otherLocales.map(locale => (
            <Route
              key={locale}
              path={`/${locale}/`}
              render={props => <App component={component} {...props} locale={locale} section="app" />}
            />
          ))}

          { primaryLocale && (
            <Route
              key={primaryLocale}
              path="/"
              render={props => <App component={component} {...props} locale={primaryLocale} section="app" />}
            />
          )}

          {/* If no valid locale is given, we redirect to same route with the preferred locale prefixed */}
          <Route render={({ location }) => <Redirect to={`/${window.__PREFERRED_LOCALE__ || otherLocales[0]}${location.pathname}`} />} />
        </Switch>
      </ApolloProvider>
    </Router>
  );

  render(<ClientApp component={Menu} />, document.getElementById('menu'));

  hydrate(<ClientApp component={HeaderTitle} />, document.getElementById('header-title'));
  hydrate(<ClientApp component={LanguagePicker} />, document.getElementById('header-lang-picker'));

  hydrate(<ClientApp component={Routes} />, document.getElementById('main'));
}

onPageLoad(() => {
  const renderStart = Date.now();
  const startupTime = renderStart - window.performance.timing.responseStart;
  console.log(`Meteor.startup took: ${startupTime}ms`);

  // Register service worker
  import('/app/ui/register-sw').then(() => {});

  renderAsync().then(() => {
    const renderTime = Date.now() - renderStart;
    console.log(`renderAsync took: ${renderTime}ms`);
    console.log(`Total time: ${startupTime + renderTime}ms`);
  });
});


from How do I implement Sibling Component communication in App shell and SSR

No comments:

Post a Comment