Flight 3

The major release 3.0.0 contains some breaking changes that you need to be aware of when upgrading. Just follow these steps and upgrading should be a breeze! So, it's finally here! The new major version of Flight, which brings a whole new development setup with Razzle, as well as a lot of performance improvements. From now on, we will version the packages together, so it's easier to figure out what the latest version of all packages are when you upgrade.

Upgrading the dependencies

Start off by upgrading your dependencies:

Copy
Copied
  "@jetshop/core": "^3.0.0",
  "@jetshop/intl": "^3.0.0",
  "@jetshop/react-scripts": "^3.0.0",
  "@jetshop/ui": "^3.0.0",

Then run yarn to install the new versions.

Change server entry point

  1. Open src/server.js
  2. Replace import boot from '@jetshop/core/boot/server'; with import { createApp } from '@jetshop/core/boot/server';
  3. Replace boot with createApp and make it a default export.

You should end up with something like this:

Copy
Copied
import React from 'react';
import { createApp } from '@jetshop/core/boot/server';
import Shop from './components/Shop';
import config from './shop.config';
import Theme from './components/Theme';

export default createApp(
  <Theme>
    <Shop />
  </Theme>,
  config
);

Update index.html

We have changed how the order of script tags etc are loaded into the HTML. Make sure that your index.html looks like this:

Copy
Copied
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <slot name="title" />
    <slot name="polyfill" />
    <slot name="preloadLinks" />

    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />

    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <slot name="meta" />
    <slot name="style" />
  </head>

  <body class="no-js">
    <script>
      document.body.className = document.body.className.replace('no-js', '');
    </script>

    <div id="root">
      <slot name="html" />
    </div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
    <slot name="apolloState" />
    <slot name="preloadScripts" />
  </body>
</html>

The really important part here is the <slot> tags. These lets the server side renderer know where to inject different parts of the data, and if they are not placed in the correct place you might get some really weird behaviours.

Nosto and Addwish

Nosto and Addwish have been removed from flight. To figure out how to add them back in please visit the Nosto Docs and the Addwish Docs

gql utility

The gql utility has been removed from the @jetshop/core package. The reason for this is that we don't want to ship an entire GraphQL parser/interpreter with every page load. Instead, put all your queries in .gql files and import them into your .js files.

Shortcodes

Shortcodes must also be updated to the latest version 2.0.5. They way shortcodes are imported has also changed. Please change your imports to the following:

Copy
Copied
import { Shortcodes } from '@jetshop/flight-shortcodes';

Be cautious about warnings

A lot of things have changed under the hood. We try to give helpful warnings and errors, so make sure that you try out the shop thoroughly, read the warnings and fix them to have a smooth transition.

Common Issues when upgrading

TypeError: Cannot read property 'theme' of null

This is usually caused by forgetting to wrap your theme around the Shop in the client.js and server.js file. Please have them look like so:

Copy
Copied
import Theme from './components/Theme';

boot(
  <Theme>
    <Shop />
  </Theme>,
  config
);

Error: You should not use Route or withRouter() outside a Router

This is caused by having a conflicting version of react-router in your package.json. Please remove any local copies of react-router as we import it in @jetshop/core

Let us know if you have any problems!

We have tried making this upgrade as smooth as possible, but there might be things we have missed. Let us know if you have any problems upgrading and we'll try to help you!

Upgrading to Flight 3.3.0

In Flight 3.3.0, the markup used to render pagination was changed for improved SEO. Instead of rendering buttons for the next and previous actions, anchors are now rendered instead. This may mean you need to update your CSS.

The disabled next/prev button will have a data-disabled="true" attribute, which you can target in your css, for example:

Copy
Copied
.pagination [data-disabled='true'] {
  cursor: not-allowed;
  opacity: 0.3;
}

There is one small breaking change when upgrading to flight 3.3.0. react-apollo-hooks has been replaced with @apollo/react-hooks. If you are using the former in your shop simply replacing the import should fix all instances of the useQuery mutation. For uses of the useMutation hook, simply wrap your mutate function in brackets like so:

Copy
Copied
const [mutate] = useMutation(mutation);

Additionally the data object coming back from apollo can sometimes be null. Please add the following null check to CategoryPage.js

before:

Copy
Copied
<ProductGrid categoryPath={category.isDynamic ? null : data.route} />

after:

Copy
Copied
<ProductGrid categoryPath={category.isDynamic ? null : data && data.route} />

Product configurations have now also been added. To use them in your shop add the ProductConfigurationsFragment.gql to your ProductPageFragment like so:

Copy
Copied
#import "@jetshop/core/data/fragments/ProductConfigurationsFragment.gql"

fragment ProductPage on Product {
  ...ProductConfigurations
}

Then update your ProductPage.js with the following new components and hooks:

Copy
Copied
const Product = ({ result: { data, loading }, product }) => {
  const price = useDynamicPrice(product);
  return (
    <div>
      <Price {...price} />
      {product.hasConfigurations && (
        <ConfigSwitch
          components={{
            default: DefaultPicker
          }}
        />
      )}
    </div>
  );
};

Upgrading to Flight 3.5.0

If you are using freeShippingLimit from countrySettings it is now no longer on the selectedChannel object. You can get it by using the useFreeShippingCheck hook.

Upgrading to Flight 3.5.1

Copy
Copied
class LogInPage extends Component {
  render() {
    return (
      <AuthContext.Consumer>
        {({ loggedIn }) => (
          <MaxWidth css={{ alignItems: 'center', marginTop: '2em' }}>
            {loggedIn ? (
              <Redirect to="/my-pages" />
            ) : (
              <LogInFormProvider>
                {({ globalError, isSubmitting, isValid }) => (

to:

Copy
Copied
class LogInPage extends Component {
  render() {
    return (
       <MaxWidth css={{ alignItems: 'center', marginTop: '2em' }}>
           <LogInFormProvider redirect="my-pages">
             {({ globalError, isSubmitting, isValid }) => (

Upgrading to Flight 3.5.2

Handling login redirects has been added to the LoginFormProvider component. To uptake this convert LoginPage.js to no longer redirect to my-pages and instead pass it as a prop if you would like to keep this behaviour

Upgrading to Flight 3.6.0 - Image component changes

The Image component has been rewritten in Flight 3.6.0 to use an img tag instead of a div with a background-image. This has been done for performance reasons, and also to add support for Chrome's new native lazy loading.

The new Image component accepts the same props as before and should behave in the same way, however the changes necessitated an update to the DOM output for the component. This means you may need to update any CSS you used to override the display of images or their children.

The new DOM structure for the image component is this:

Copy
Copied
<div
  data-flight-image-container=""
  data-flight-image-loaded="true"
  style="position: relative; overflow:hidden; width:100%; height:auto; padding-bottom: 100%"
>
  <!-- The LQIP (Low Quality Image Placeholder) -->
  <!-- This shows a tiny blurred version of the full image -->
  <picture data-flight-image-placeholder="">
    <img
      alt=""
      src="https://www.demostore.se/pub_images/original/842086630.jpg?extend=copy&amp;width=160&amp;method=fit&amp;height=160&amp;sigma=2.5&amp;minampl=0.5&amp;quality=30"
      loading="auto"
      style='position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: contain; object-position: center center; opacity: 0; transition-delay: 500ms; font-family: "object-fit: contain";'
    />
  </picture>
  <!-- The full image -->
  <!-- This renders only when the image is in the viewport, OR if native lazy loading is supported -->
  <picture data-flight-image="">
    <source
      type="image/webp"
      srcset="
        https://www.demostore.se/pub_images/original/842086630.jpg?extend=copy&amp;width=160&amp;method=fit&amp;height=160&amp;type=webp 160w …etc
      "
      sizes="(max-width: 20rem) 50vw, (max-width: 40rem) 50vw, (max-width: 50rem) 33vw, 25vw"
    />
    <source
      srcset="
        https://www.demostore.se/pub_images/original/842086630.jpg?extend=copy&amp;width=160&amp;method=fit&amp;height=160 160w …etc
      "
      sizes="(max-width: 20rem) 50vw, (max-width: 40rem) 50vw, (max-width: 50rem) 33vw, 25vw"
    />
    <img
      alt="Image Alt"
      sizes="(max-width: 20rem) 50vw, (max-width: 40rem) 50vw, (max-width: 50rem) 33vw, 25vw"
      srcset="
        https://www.demostore.se/pub_images/original/842086630.jpg?extend=copy&amp;width=160&amp;method=fit&amp;height=160 160w …etc
      "
      src="https://www.demostore.se/pub_images/original/842086630.jpg"
      loading="lazy"
      style='position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: contain; object-position: center center; opacity: 1; transition: opacity 500ms ease 0s; font-family: "object-fit: contain";'
    />
  </picture>
  <!-- The NOSCRIPT tag loads the full image when JS is disabled -->
  <noscript>
    <picture>
      <img
        alt="842086630"
        sizes="(max-width: 20rem) 50vw, (max-width: 40rem) 50vw, (max-width: 50rem) 33vw, 25vw"
        srcset="
          https://www.demostore.se/pub_images/original/842086630.jpg?extend=copy&amp;width=160&amp;method=fit&amp;height=160 160w …etc
        "
        src="https://www.demostore.se/pub_images/original/842086630.jpg"
        loading="lazy"
        style="position:absolute; top:0; left:0; width:100%; height:100%; object-fit:contain; object-position:center"
      />
    </picture>
  </noscript>
  <!-- Any children of the image component are rendered inside this container -->
  <div data-flight-image-children="" style="position:relative; z-index:2">
    Image component children render here! The container is given a z-index of 2
    to ensure it appears over the top of the img
  </div>
</div>

Note that each of the key elements has a data attribute. You can use this in your CSS to target those parts of the DOM.

For example, in your CSS:

Copy
Copied
.image [data-flight-image-children] {
  font-size: 200%;

  p {
    line-height: 1.75;
  }
}
.image [data-flight-image-placeholder] {
  opacity: 0.1;
}

In your JSX:

Copy
Copied
<Image {...restOfYourImageprops} className="image">
  <p>This text will have a font-size of 200% and line-height of 1.75</p>
</Image>

Upgrading to Flight 3.7.0 - New filter implementation

In the Flight 3.7.0, the filter implementation has been rewritten to improve the developer experience, as well as to improve performance. Upgrading to the new version of filters is opt-in for now, but the old implementation will be removed in a future major release.

The previous implementation of filters was quite complex, and so the complexity was hidden away inside the Flight Core and UI packages. The new implementation has a much simpler API, and so the UI has been moved in to the template. This allows you to create any UI or styling that you like.

The new implementation uses Apollo's local state management. If you're interested, you can read the docs on Apollo's website. You don't need to know how this works in order to use the new filters, but you may find it interesting!

In order to switch to the new implementation, there are a few steps to take.

  1. Remove references to FilterContext and FilterContainer

Filters no longer use React context to manage state. These components should be removed.

Note: if you were previously using FilterContext to enable global filters, you will have to use the previous implementation for now. Global filters will be enabled for the new API in a coming release.

  1. Remove all UI filter components

The filter components at @jetshop/ui are using the now-deprecated API. In order to update, you should make your own components for list, boolean, range, and multi list filters. You'll also need to make your own active filters component.

The easiest way to do this is to copy the components from Template Trend. As all styling and UI is now in those template files, you are able to customise them much more easily than before.

  1. Update your graphql queries

You'll also need to update the CategoryPageFragment and SearchQuery to add the new @client fields. You can see examples of the CategoryPageFragment and the SearchQuery on the main Flight repo.

These client fields are critical to the new implementation, and allow you to query for the filter state alongside the data returned from the API.

For example, this allows you to determine is a ListFilter is active by checking the hasActiveItems field on the filter itself.

Filter hooks

The new API uses custom hooks to provide all of the functions you will need to apply and clear filters. These replace the previous React context-based implementation.

For example, the previous implementation of boolean filters looked like this:

Copy
Copied
<FilterContext.Consumer>
  {({ activeFilters, applyBoolFilter }) =>
    boolFilters.map((filter) => {
      const isSelected = activeFilters.booleanFilters.some(
        (item) => item.id === stripCategoryId(filter.id)
      );

      return (
        <Checkbox
          key={filter.id}
          label={filter.name}
          checked={isSelected}
          onChange={(e) => applyBoolFilter(filter.id, e.currentTarget.checked)}
        />
      );
    })
  }
</FilterContext.Consumer>

The new implementation looks like this:

Copy
Copied
function BooleanFilter({ filter }) {
  const { apply } = useBooleanFilter({ filter });
  return (
    <Checkbox
      key={filter.id}
      label={filter.name}
      checked={filter.value}
      onChange={(e) => {
        apply({ value: e.currentTarget.checked });
      }}
    />
  );
}

The 2 key things to notice here are:

  1. The new useBooleanFilter hook takes in the filter and returns an apply function, which can be used to apply the filter.
  2. The filter has a new (client-only) value field, which returns a boolean (true if the boolean filter is applied, false if it is not). The replaces the need for the loop through activeFilters to determine whether the boolean filter is applied or not.

All of the filters have their own respective custom hooks — useBooleanFilter, useListFilter, useRangeFilter, useMultiFilter.

There is also a useFilters hook, which can be used to clear all filters:

Copy
Copied
const { clearAllFilters } = useFilters();

return <button onClick={clearAllFilters}>Clear Filters</button>;

Full documentation

Please see the full documentation for these hooks here.

Upgrading to Flight 3.8.0

react-router has been upgraded to version 5.1, which now includes built in support for hooks. This means that the package we have been using for hook support use-react-router has been removed and is no longer included in dependencies.

If you're using My pages from flight, you have to make a small change to MyPages.js:

Copy
Copied
// replace
import useReactRouter from 'use-react-router';
// with
import { useLocation } from 'react-router';

function MyPages() {
  const { loggedIn } = useAuth();
  // replace
  const { location } = useReactRouter();
  // with
  const location = useLocation();

  // rest of the component
}

If you use use-react-router somewhere else in the code, make sure to replace those occurances with useLocation, useHistory, useParams or useRouteMatch depending on what data you need. Read more at the React Router Hooks API documentation.

Copyright © Norce 2023. All right reserved.