SaltyCrane Blog — Notes on JavaScript and web development

How to generate static HTML using React, TypeScript, and Node.js

React is used to build web applications that run JavaScript in a user's browser (client side rendering). It can also be used from a Node.js script to generate static HTML (static rendering). I used this technique to generate some CSS width experiments and a TypeScript Next.js cheat sheet. The example below shows how to use React with TypeScript and Node.js to generate static HTML. I also made an example project on github.

Install Node.js

  • $ brew install node

Set up project

  • Create a project directory and cd into it
    $ mkdir my-project
    $ cd my-project
  • Create a package.json file:
    {
      "scripts": {
        "render": "tsc && node dist/render.js"
      },
      "dependencies": {
        "@types/node": "^14.0.4",
        "@types/prettier": "^2.0.0",
        "@types/react": "^16.9.35",
        "@types/react-dom": "^16.9.8",
        "prettier": "^2.0.5",
        "react": "^16.13.1",
        "react-dom": "^16.13.1",
        "typescript": "^3.9.3"
      }
    }
  • Install React, TypeScript, and other packages
    $ npm install
  • Create a tsconfig.json file to configure TypeScript
    {
      "compilerOptions": {
        "baseUrl": ".",
        "esModuleInterop": true,
        "jsx": "react",
        "lib": ["dom", "es2019"],
        "outDir": "dist",
        "paths": {
          "*": ["src/*", "src/@types/*"]
        }
      },
      "include": ["src/*.tsx"]
    }

Create a script to generate a static HTML file

  • Create a directory, src and a file src/render.tsx:
    import * as fs from "fs";
    import prettier from "prettier";
    import React from "react";
    import ReactDOMServer from "react-dom/server";
    
    render();
    
    function render() {
      let html = ReactDOMServer.renderToStaticMarkup(<HelloWorldPage />);
      let htmlWDoc = "<!DOCTYPE html>" + html;
      let prettyHtml = prettier.format(htmlWDoc, { parser: "html" });
      let outputFile = "./output.html";
      fs.writeFileSync(outputFile, prettyHtml);
      console.log(`Wrote ${outputFile}`);
    }
    
    function HelloWorldPage() {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <title>Hello world</title>
          </head>
          <body>
            <h1>Hello world</h1>
          </body>
        </html>
      );
    }

Run the script and view the output

  • Run the script
    $ npm run render
  • Open the output.html file in the browser
    $ open output.html

How width is set in CSS

In CSS, I'm often confused about when an element adjusts to the width of its container and when it adjusts to the width of its content. I made some experiments to test the CSS behavior in various conditions. The script to generate the experiments is on github and the results are shown below. See also my companion page on CSS height.

By default, elements adjust to the width of their container when they are
By default, elements adjust to width of their content when they are
Some elements can be made to adjust to the width of their container by
Some elements can be made to adjust to the width of their content by
For some cases, setting overflow limits an element's width to the width of its container for wide content:
Some miscellaneous cases:

See also

Block containers

Block containers - block elements in block containers adjust to the width of their container by default
#container-1a
#example-1a
#content-1a
HTML
<div id="container-1a"> <div id="example-1a"> <div id="content-1a" /> </div> </div>
CSS
#container-1a { display: block; /* default */ width: 400px; } #example-1a { display: block; /* default */ } #content-1a { width: 120px; }
#container-1b
#example-1b
#content-1b
HTML
<div id="container-1b"> <div id="example-1b"> <div id="content-1b" /> </div> </div>
CSS
#container-1b { display: block; /* default */ width: 400px; } #example-1b { display: block; /* default */ } #content-1b { width: 420px; }
Block containers, fit-content - block elements in block containers adjust to the width of their container by default, but can use fit-content to adjust to the width of their content. MDN docs on width. NOTE: fit-content is not supported by IE.
#container-1c
#example-1c
#content-1c
HTML
<div id="container-1c"> <div id="example-1c"> <div id="content-1c" /> </div> </div>
CSS
#container-1c { display: block; /* default */ width: 400px; } #example-1c { display: block; /* default */ width: fit-content; } #content-1c { width: 120px; }
#container-1d
#example-1d
#content-1d
HTML
<div id="container-1d"> <div id="example-1d"> <div id="content-1d" /> </div> </div>
CSS
#container-1d { display: block; /* default */ width: 400px; } #example-1d { display: block; /* default */ width: fit-content; } #content-1d { width: 420px; }

Inline elements

Inline elements - inline elements adjust to the width of their content by default
#container-2a
#example-2a
#content-2a
HTML
<div id="container-2a"> <div id="example-2a"> <div id="content-2a" /> </div> </div>
CSS
#container-2a { display: block; /* default */ width: 400px; } #example-2a { display: inline-block; } #content-2a { width: 120px; }
#container-2b
#example-2b
#content-2b
HTML
<div id="container-2b"> <div id="example-2b"> <div id="content-2b" /> </div> </div>
CSS
#container-2b { display: block; /* default */ width: 400px; } #example-2b { display: inline-block; } #content-2b { width: 420px; }
Inline elements, width 100% - inline elements adjust to the width of their container if width is set to 100%
#container-2c
#example-2c
#content-2c
HTML
<div id="container-2c"> <div id="example-2c"> <div id="content-2c" /> </div> </div>
CSS
#container-2c { display: block; /* default */ width: 400px; } #example-2c { display: inline-block; width: 100%; } #content-2c { width: 120px; }
#container-2d
#example-2d
#content-2d
HTML
<div id="container-2d"> <div id="example-2d"> <div id="content-2d" /> </div> </div>
CSS
#container-2d { display: block; /* default */ width: 400px; } #example-2d { display: inline-block; width: 100%; } #content-2d { width: 420px; }

Flex row containers

Flex row container - elements in flex row containers adjust to the width of their content by default
#container-3a
#example-3a
#content-3a
HTML
<div id="container-3a"> <div id="example-3a"> <div id="content-3a" /> </div> </div>
CSS
#container-3a { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3a { display: block; /* default */ } #content-3a { width: 120px; }
#container-3b
#example-3b
#content-3b
HTML
<div id="container-3b"> <div id="example-3b"> <div id="content-3b" /> </div> </div>
CSS
#container-3b { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3b { display: block; /* default */ } #content-3b { width: 420px; }
Flex row container, width 100% - elements in flex row containers adjust to the width of their content by default. Setting width to 100%causes them to adjust to the width of their container.
#container-3c
#example-3c
#content-3c
HTML
<div id="container-3c"> <div id="example-3c"> <div id="content-3c" /> </div> </div>
CSS
#container-3c { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3c { display: block; /* default */ width: 100%; } #content-3c { width: 120px; }
#container-3d
#example-3d
#content-3d
HTML
<div id="container-3d"> <div id="example-3d"> <div id="content-3d" /> </div> </div>
CSS
#container-3d { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3d { display: block; /* default */ width: 100%; } #content-3d { width: 420px; }
Flex row container, flex-grow - elements in flex row containers adjust to the width of their content by default. Setting flex-grow to 1 causes them to expand to the width of their container if their content is narrower than their container. MDN docs on flex-grow.
#container-3e
#example-3e
#content-3e
HTML
<div id="container-3e"> <div id="example-3e"> <div id="content-3e" /> </div> </div>
CSS
#container-3e { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3e { display: block; /* default */ flex-grow: 1; } #content-3e { width: 120px; }
If content is wider than the container, the element expands to the width of their content.
#container-3f
#example-3f
#content-3f
HTML
<div id="container-3f"> <div id="example-3f"> <div id="content-3f" /> </div> </div>
CSS
#container-3f { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3f { display: block; /* default */ flex-grow: 1; } #content-3f { width: 420px; }
Flex row container, flex-grow, overflow - setting overflow to something other than visible limits the width of the element to the width of its container.
#container-3g
#example-3g
#content-3g
HTML
<div id="container-3g"> <div id="example-3g"> <div id="content-3g" /> </div> </div>
CSS
#container-3g { display: flex; flex-direction: row; /* default */ justify-content: normal; /* default - behaves as flex-start in this case */ width: 400px; } #example-3g { display: block; /* default */ flex-grow: 1; overflow: auto; } #content-3g { width: 420px; }

Flex column containers

Flex column container - elements in flex column containers adjust to the width of their container by default
#container-4a
#example-4a
#content-4a
HTML
<div id="container-4a"> <div id="example-4a"> <div id="content-4a" /> </div> </div>
CSS
#container-4a { display: flex; align-items: normal; /* default - behaves like stretch in this case */ flex-direction: column; width: 400px; } #example-4a { display: block; /* default */ } #content-4a { width: 120px; }
#container-4b
#example-4b
#content-4b
HTML
<div id="container-4b"> <div id="example-4b"> <div id="content-4b" /> </div> </div>
CSS
#container-4b { display: flex; align-items: normal; /* default - behaves like stretch in this case */ flex-direction: column; width: 400px; } #example-4b { display: block; /* default */ } #content-4b { width: 420px; }
Flex column container, align-items - elements in a flex column container adjust to the width of the container by default, but setting align-items to something other than stretch (e.g. flex-start), causes the element to adjust to the width of its content. MDN docs on align-items.
#container-4c
#example-4c
#content-4c
HTML
<div id="container-4c"> <div id="example-4c"> <div id="content-4c" /> </div> </div>
CSS
#container-4c { display: flex; align-items: flex-start; flex-direction: column; width: 400px; } #example-4c { display: block; /* default */ } #content-4c { width: 120px; }
#container-4d
#example-4d
#content-4d
HTML
<div id="container-4d"> <div id="example-4d"> <div id="content-4d" /> </div> </div>
CSS
#container-4d { display: flex; align-items: flex-start; flex-direction: column; width: 400px; } #example-4d { display: block; /* default */ } #content-4d { width: 420px; }

Grid containers

Grid container - elements in a grid container expand to the width of their container if their content is narrower than their container.
#container-5a
#example-5a
#content-5a
HTML
<div id="container-5a"> <div id="example-5a"> <div id="content-5a" /> </div> </div>
CSS
#container-5a { display: grid; justify-items: normal; /* default - behaves like stretch in this case */ width: 400px; } #example-5a { display: block; /* default */ } #content-5a { width: 120px; }
If the content is wider than the container, the element expands to the width of their content instead.
#container-5b
#example-5b
#content-5b
HTML
<div id="container-5b"> <div id="example-5b"> <div id="content-5b" /> </div> </div>
CSS
#container-5b { display: grid; justify-items: normal; /* default - behaves like stretch in this case */ width: 400px; } #example-5b { display: block; /* default */ } #content-5b { width: 420px; }
Grid container, width 100% - elements in a grid container expand to the width of their content for wide content even if width is set to 100%
#container-5c
#example-5c
#content-5c
HTML
<div id="container-5c"> <div id="example-5c"> <div id="content-5c" /> </div> </div>
CSS
#container-5c { display: grid; justify-items: normal; /* default - behaves like stretch in this case */ width: 400px; } #example-5c { display: block; /* default */ width: 100%; } #content-5c { width: 420px; }
Grid container, overflow - if content is wider than the continer, elements in a grid container adjust to the width of the container if overflow is set to something other than visible
#container-5d
#example-5d
#content-5d
HTML
<div id="container-5d"> <div id="example-5d"> <div id="content-5d" /> </div> </div>
CSS
#container-5d { display: grid; justify-items: normal; /* default - behaves like stretch in this case */ width: 400px; } #example-5d { display: block; /* default */ overflow: auto; } #content-5d { width: 420px; }
Grid container, justify-items - elements in grid containers with justify-items set to something other than stretch adjust to the width of their content. MDN docs on justify-items.
#container-5e
#example-5e
#content-5e
HTML
<div id="container-5e"> <div id="example-5e"> <div id="content-5e" /> </div> </div>
CSS
#container-5e { display: grid; justify-items: start; width: 400px; } #example-5e { display: block; /* default */ } #content-5e { width: 120px; }
#container-5f
#example-5f
#content-5f
HTML
<div id="container-5f"> <div id="example-5f"> <div id="content-5f" /> </div> </div>
CSS
#container-5f { display: grid; justify-items: start; width: 400px; } #example-5f { display: block; /* default */ } #content-5f { width: 420px; }
Grid container, justify-items, overflow - setting overflow has no effect when justify-items is set
#container-5g
#example-5g
#content-5g
HTML
<div id="container-5g"> <div id="example-5g"> <div id="content-5g" /> </div> </div>
CSS
#container-5g { display: grid; justify-items: start; width: 400px; } #example-5g { display: block; /* default */ overflow: auto; } #content-5g { width: 420px; }

Absolute positioning

Absolutely positioned - when position: absolute is used, the element adjusts to the width of its content. MDN docs on position: absolute.
#container-6a
#example-6a
#content-6a
HTML
<div id="container-6a"> <div id="example-6a"> <div id="content-6a" /> </div> </div>
CSS
#container-6a { display: block; /* default */ position: relative; width: 400px; } #example-6a { display: block; /* default */ position: absolute; } #content-6a { width: 120px; }
#container-6b
#example-6b
#content-6b
HTML
<div id="container-6b"> <div id="example-6b"> <div id="content-6b" /> </div> </div>
CSS
#container-6b { display: block; /* default */ position: relative; width: 400px; } #example-6b { display: block; /* default */ position: absolute; } #content-6b { width: 420px; }

Floated elements

Floated elements - elements that set float adjust to the width of their content. MDN docs on float.
#container-7a
#example-7a
#content-7a
HTML
<div id="container-7a"> <div id="example-7a"> <div id="content-7a" /> </div> </div>
CSS
#container-7a { display: block; /* default */ width: 400px; } #example-7a { display: block; /* default */ float: left; } #content-7a { width: 120px; }
#container-7b
#example-7b
#content-7b
HTML
<div id="container-7b"> <div id="example-7b"> <div id="content-7b" /> </div> </div>
CSS
#container-7b { display: block; /* default */ width: 400px; } #example-7b { display: block; /* default */ float: left; } #content-7b { width: 420px; }

COVID-19 and kids links

Data

Tools

Government and health authorities

Kids

How to remount a React component when a prop changes

To remount a component when a prop changes, use the React key attribute as described in this post on the React blog:

When a key changes, React will create a new component instance rather than update the current one.

The example below shows how the key attribute can be used. In Parent, the key attribute of <Child> is set to String(primaryExists). When primaryExists changes in the parent component, the child component unmounts and remounts allowing useState to re-initialize its state with the intial value passed in from props (!primaryExists). Play with it on CodeSandbox.

import React, { useState } from "react";

function Parent() {
  // Note: in my real code, primaryExists was derived from API data,
  // but I useState here to simplify the example
  const [primaryExists, setPrimaryExists] = useState(true);
  return (
    <div>
      <label>
        <input
          checked={primaryExists}
          onChange={() => setPrimaryExists(x => !x)}
          type="checkbox"
        />
        Primary exists
      </label>
      <Child key={String(primaryExists)} primaryExists={primaryExists} />
    </div>
  );
}

function Child({ primaryExists }) {
  const [isPrimary, setIsPrimary] = useState(!primaryExists);
  return (
    <div>
      <label>
        <input
          checked={isPrimary}
          onChange={() => setIsPrimary(x => !x)}
          type="checkbox"
        />
        Is primary
      </label>
    </div>
  );
} 

Additional information

Sebastian Markbåge, from the React core team, reinforced the usage of keys on single items:

...BUT it has given the misinterpretation that only lists need keys. You need keys on single items too. In a master/detail, the detail should have a key.

React hook to fit text in a div

This is a React hook that iteratively adjusts the font size so that text will fit in a div.

  • checks if text is overflowing by using scrollHeight and offsetHeight from https://stackoverflow.com/a/10017343/101911
  • uses binary search; makes a maximum of 5 adjustments with a resolution of 5% font size from 20-100%

The code is also in a github repo: use-fit-text

const useFitText = () => {
  const MIN_FONT_SIZE = 20;
  const MAX_FONT_SIZE = 100;
  const RESOLUTION = 5;

  const ref = useRef(null);

  const [state, setState] = useState({
    fontSize: MAX_FONT_SIZE,
    fontSizePrev: MIN_FONT_SIZE,
    fontSizeMax: MAX_FONT_SIZE,
    fontSizeMin: MIN_FONT_SIZE,
  });
  const { fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;

  useEffect(() => {
    const isDone = Math.abs(fontSize - fontSizePrev) <= RESOLUTION;
    const isOverflow =
      !!ref.current &&
      (ref.current.scrollHeight > ref.current.offsetHeight ||
        ref.current.scrollWidth > ref.current.offsetWidth);
    const isAsc = fontSize > fontSizePrev;

    // return if the font size has been adjusted "enough" (change within RESOLUTION)
    // reduce font size by one increment if it's overflowing
    if (isDone) {
      if (isOverflow) {
        const fontSizeNew =
          fontSizePrev < fontSize
            ? fontSizePrev
            : fontSize - (fontSizePrev - fontSize);
        setState({
          fontSize: fontSizeNew,
          fontSizeMax,
          fontSizeMin,
          fontSizePrev,
        });
      }
      return;
    }

    // binary search to adjust font size
    let delta;
    let newMax = fontSizeMax;
    let newMin = fontSizeMin;
    if (isOverflow) {
      delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;
      newMax = Math.min(fontSizeMax, fontSize);
    } else {
      delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;
      newMin = Math.max(fontSizeMin, fontSize);
    }
    setState({
      fontSize: fontSize + delta / 2,
      fontSizeMax: newMax,
      fontSizeMin: newMin,
      fontSizePrev: fontSize,
    });
  }, [fontSize, fontSizeMax, fontSizeMin, fontSizePrev, ref]);

  return { fontSize: `${fontSize}%`, ref };
};

Example usage

import React from "react";
import useFitText from "use-fit-text";

const Example = () => {
  const { fontSize, ref } = useFitText();

  return (
    <div ref={ref} style={{ fontSize, height: 40, width: 100 }}>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </div>
  );
}

How to useMemo to improve the performance of a React table

These are notes on how I improved the React rendering time of a large table with useMemo, the React Devtools, and consideration of referential equality of my data.

Summary of steps

  • Profile using the React Devtools Profiler to find components that are rendering excessively
  • Add memo, useMemo, or PureComponent to prevent the excessive rendering
  • If using memo or PureComponent, ensure the props passed in are referentially equal. Something like use-why-did-you-update can help find unexpected inequalities. If using useMemo, ensure the dependencies in the dependency array are referentially equal. Hooks like useMemo and useCallback can help preserve referential equality of props or dependencies. If using Redux, reselect memoizes selectors to prevent excessive referential inequalities. And immutable libraries like immer help preserve referential equality by preserving references to data if the values do not change.

Problem

I had a table of 100 rows of select inputs. Changing a select input had a noticeable lag.

react table screenshot

React Profiler

I profiled the table with the Profiler in React Devtools and found that all the rows were re-rendering even though only one of them changed. The screenshot below shows rendering of my Table component took 239ms. All the colored bars beneath the Table means each of the 100 rows are rendering even though only one of them changed. For more information, see this article on how to use the React Profiler.

react table screenshot

Row component

The table was built using React hooks and I sprinkled useMemo liberally in my code. Most of my data was memoized, but React was still re-rendering. Here is my row component:

const MappingRow = ({ id }) => {
  // ...
  const mapping = useMapping(state, id);
  const enabled = useEnabledFields(state, id);
  const { makeOptions, modelFamilyOptions, modelParentOptions, segmentOptions } = useMappingRowApis(id);

  const handleChange = field => selected => {
    const value = selected && selected.value;
    if (value === mapping[field]) {
      return;
    }
    const update = { [field]: value };
    dispatch({
      type: "save_mapping",
      promise: saveMapping(id, update),
      id,
      timeSaved: Date.now(),
      update,
    });
  };

  return (
    <tr>
      <Cell>{mapping.source}</Cell>
      <SelectCell
        isDisabled={!enabled.mappableMakeName}
        onChange={handleChange("makeCode")}
        options={makeOptions}
        value={mapping.makeCode}
      />
      <SelectCell
        isDisabled={!enabled.mappableModelParent}
        onChange={handleChange("modelParentId")}
        options={modelParentOptions}
        value={mapping.modelParentId}
      />
      <SelectCell
        isDisabled={!enabled.mappableModelFamilyName}
        onChange={handleChange("modelFamilyName")}
        options={modelFamilyOptions}
        value={mapping.modelFamilyName}
      />
      <SelectCell
        isDisabled={!enabled.mappableSegmentName}
        onChange={handleChange("segmentCode")}
        options={segmentOptions}
        value={mapping.segmentCode}
      />
    </tr>
  );
};

memo HOC

Even though the data provided by my custom hooks was memoized, I realized I still needed to apply React's memo higher order component (HOC) to prevent re-rendering. I extracted out a new MemoizedRow component, so that I could wrap it with React's memo HOC. (Note: if this seems undesirable to you, see the end of this post.)

const MappingRow = ({ id }) => {
  // ...
  const mapping = useMapping(state, id);
  const enabled = useEnabledFields(state, id);
  const { makeOptions, modelFamilyOptions, modelParentOptions, segmentOptions } = useMappingRowApis(id);
  const handleChange = field => selected => {
    // ...
  };

  return (
    <MemoizedRow
      enabled={enabled}
      handleChange={handleChange}
      makeOptions={makeOptions}
      mapping={mapping}
      modelFamilyOptions={modelFamilyOptions}
      modelParentOptions={modelParentOptions}
      segmentOptions={segmentOptions}
    />
  );
};

const MemoizedRow = memo(props => {
  const {
    enabled,
    handleChange,
    makeOptions,
    mapping,
    modelFamilyOptions,
    modelParentOptions,
    segmentOptions,
    sourceConfig,
  } = props;
  return (
    <tr>
      <Cell>{mapping.source}</Cell>
      <SelectCell
        isDisabled={!enabled.mappableMakeName}
        onChange={handleChange("makeCode")}
        options={makeOptions}
        value={mapping.makeCode}
      />
      <SelectCell
        isDisabled={!enabled.mappableModelParent}
        onChange={handleChange("modelParentId")}
        options={modelParentOptions}
        value={mapping.modelParentId}
      />
      <SelectCell
        isDisabled={!enabled.mappableModelFamilyName}
        onChange={handleChange("modelFamilyName")}
        options={modelFamilyOptions}
        value={mapping.modelFamilyName}
      />
      <SelectCell
        isDisabled={!enabled.mappableSegmentName}
        onChange={handleChange("segmentCode")}
        options={segmentOptions}
        value={mapping.segmentCode}
      />
    </tr>
  );
});

Referential equality or shallow equality

I applied the memo HOC, but profiling showed no change in performance. I thought I should useWhyDidYouUpdate. This revealed some of my props were not equal when I expected them to be. One of them was my handleChange callback function. This function is created every render. The reference to the function from one render does not compare as equal to the reference to the function in another render. Wrapping this function with useCallback memoized the function so it will compare equally unless one of the dependencies change (mapping or id).

const MappingRow = ({ id }) => {
  //...

  const handleChange = useCallback(
    field => selected => {
      const value = selected && selected.value;
      if (value === mapping[field]) {
        return;
      }
      const update = { [field]: value };
      dispatch({
        type: "save_mapping",
        promise: saveMapping(id, update),
        id,
        timeSaved: Date.now(),
        update,
      });
    },
    [mapping, id],
  );

  return (
    <MemoizedRow
      enabled={enabled}
      handleChange={handleChange}
      makeOptions={makeOptions}
      mapping={mapping}
      modelFamilyOptions={modelFamilyOptions}
      modelParentOptions={modelParentOptions}
      segmentOptions={segmentOptions}
    />
  );
};

Another problem was my mapping data object was changing for every row even though I only actually changed one of the rows. I was using the Immer library to create immutable data structures. I had learned that using immutable data structures allows updating a slice of data in an object without changing the reference to a sibling slice of data so that it would compare equally when used with the memo HOC or PureComponent. I had thought my data was properly isolated and memoized, however there was one piece of my state that was breaking the memoization. Here is my code to return a single mapping data object for a row:

export const useMapping = (state, id) => {
  const {
    optimisticById,
    optimisticIds,
    readonlyById,
    writableById,
  } = state.mappings;
  const optimisticMapping = optimisticById[id];
  const readonlyMapping = readonlyById[id];
  const writableMapping = writableById[id];
  return useMemo(() => {
    const mapping = { ...readonlyMapping, ...writableMapping };
    return optimisticIds.includes(id)
      ? { ...mapping, ...optimisticMapping }
      : mapping;
  }, [id, optimisticIds, optimisticMapping, readonlyMapping, writableMapping]);
};

The optimisticIds state was used to store a list of ids of mapping items that had been updated by the user, but had not yet been saved to the database. This list changed whenever a row was edited, but it was used in creating the mapping data for every row in the table. The optimisticIds is in the useMemo dependency array, so when it changes, the mapping data is re-calculated and a new value is returned. The important part is not that running the code in this function is expensive. The important part is that the function returns a newly created object literal. Like the handleChange function created in the component above, object literals created at different times do not compare equally even if the contents of the object are the same. e.g. The following is not true in JavaScript: {} === {}. I realized I did not need the optimisticIds state, so I removed it. This left a memoized function that only recalculated when data for its corresponding row in the table changed:

export const useMapping = (state, id) => {
  const { optimisticById, readonlyById, writableById } = state.mappings;
  const optimisticMapping = optimisticById[id];
  const readonlyMapping = readonlyById[id];
  const writableMapping = writableById[id];
  return useMemo(() => {
    const mapping = { ...readonlyMapping, ...writableMapping };
    return optimisticMapping ? { ...mapping, ...optimisticMapping } : mapping;
  }, [optimisticMapping, readonlyMapping, writableMapping]);
};

20X improvement

After fixing these referential inequalities, the memo HOC eliminated the re-rendering of all but the edited row. The React profiler now showed the table rendered in 10ms, a 20X improvement.

react table screenshot

Refactoring to useMemo

To use the memo HOC, I had to extract out a separate component for the sole purpose of applying the memo HOC. I started to convert the HOC to a render prop so I could use it inline. Then I thought, aren't hooks supposed to replace most HOCs and render props? Someone should make a useMemo hook to do what the memo HOC does. Wait there is a useMemo hook already... I wonder if...

const MappingRow = ({ id }) => {
  const mapping = useMapping(id);
  const enabled = useEnabledFields(id);
  const { makeOptions, modelFamilyOptions, modelParentOptions, segmentOptions } = useMappingRowApis(id);

  const handleChange = useCallback(
    field => selected => {
      const value = selected && selected.value;
      if (value === mapping[field]) {
        return;
      }
      const update: MappingUpdate = { [field]: value };
      dispatch({
        type: "save_mapping",
        promise: saveMapping(id, update),
        id,
        timeSaved: Date.now(),
        update,
      });
    },
    [dispatch, mapping, id],
  );

  return useMemo(
    () => (
      <tr>
        <Cell>{mapping.source}</Cell>
        <SelectCell
          isDisabled={!enabled.mappableMakeName}
          onChange={handleChange("makeCode")}
          options={makeOptions}
          value={mapping.makeCode}
        />
        <SelectCell
          isDisabled={!enabled.mappableModelParent}
          onChange={handleChange("modelParentId")}
          options={modelParentOptions}
          value={mapping.modelParentId}
        />
        <SelectCell
          isDisabled={!enabled.mappableModelFamilyName}
          onChange={handleChange("modelFamilyName")}
          options={modelFamilyOptions}
          value={mapping.modelFamilyName}
        />
        <SelectCell
          isDisabled={!enabled.mappableSegmentName}
          onChange={handleChange("segmentCode")}
          options={segmentOptions}
          value={mapping.segmentCode}
        />
      </tr>
    ),
    [
      enabled,
      handleChange,
      mapping,
      makeOptions,
      modelParentOptions,
      modelFamilyOptions,
      segmentOptions,
    ],
  );
};

Yes applying useMemo to the returned JSX element tree had the same effect as applying the memo HOC without the intrusive component refactor. I thought that was pretty cool. Dan Abramov tweeted about wrapping React elements with useMemo also:

Creating a GraphQL API with Python, Graphene, and Postgres

Here are my notes on creating a GraphQL API with Python, Django, Graphene, and Postgres. I learned almost everything from the excellent GraphQL Python tutorial at howtographql.com. Optimistically, I'll write a series of posts about how to make a React Native app that no one will use. Realistically, I won't.

Contents

Install Python 3.7.2

$ brew install python

Create project directory and virtualenv

$ # make project directory
$ mkdir travelog-api
$ cd travelog-api
$ # make virtualenv
$ python3 -m venv venv
$ # activate virtualenv
$ source venv/bin/activate
$ # upgrade pip
$ pip install --upgrade pip

Install Django and create a Django project

(Still in the travelog-api directory with virtualenv activated)

  • Install Django 2.1.7:
    $ pip install Django 
    
  • Create Django project:
    $ django-admin startproject travelog_api ./
    
  • Run migrations and run the server:
    $ ./manage.py migrate
    $ ./manage.py runserver
    $ # go to http://localhost:8000 in the browser
    

Run Postgres in Docker

(Still in the travelog-api directory with virtualenv activated)

  • Install Docker for Mac
  • Create a new file, travelog-api/docker-compose.yml:
    version: "3.7"
    services:
      db:
        image: "postgres:11.2"
        container_name: "travelog_postgres1"
        ports:
          - "54321:5432"
        volumes:
          - postgres_data1:/var/lib/postgresql/data
    volumes:
      postgres_data1:
        name: travelog_postgres_data1
  • Start Postgres
    $ docker-compose up -d
    $ docker-compose logs
    

Create a database

  • Start psql:
    $ docker exec -it travelog_postgres1 psql -U postgres
    
  • Create a database (be sure to include the semicolon):
    postgres=# create database travelog;
  • Create user:
    postgres=# create user traveloguser with password 'mypassword';
    postgres=# grant all privileges on database travelog to traveloguser;
  • Exit psql:
    postgres=# \q

Configure Django to use Postgres

(Still in the travelog-api directory with virtualenv activated)

  • Install psycopg2 2.7.7:
    $ pip install psycopg2-binary
    
  • Edit travelog-api/travelog_api/settings.py:
    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.postgresql_psycopg2",
            "NAME": "travelog",
            "USER": "traveloguser",
            "PASSWORD": "mypassword,
            "HOST": "localhost",
            "PORT": "54321",
        }
    }
    
  • Run database migrations and run the server:
    $ ./manage.py migrate
    $ ./manage.py runserver
    $ # go to http://localhost:8000 in the browser
    

Install and configure Graphene

(Still in the travelog-api directory with virtualenv activated)

  • Install graphene-django 2.2.0
    $ pip install graphene-django
    
  • Edit the INSTALLED_APPS setting in travelog-api/travelog_api/settings.py:
    INSTALLED_APPS = (
        # After the default packages
        "graphene_django",
    )
    

Create a new Django app and add a model

(Still in the travelog-api directory with virtualenv activated)

  • Create a new Django app:
    $ ./manage.py startapp geo
    
  • Edit travelog_api/settings.py:
    INSTALLED_APPS = (
        # After the default packages
        'graphene_django',
        'geo',
    )
    
  • Edit travelog-api/geo/models.py:
    from django.db.models import DateTimeField, FloatField, Model, TextField
    
    class Location(Model):
        created_at = DateTimeField(auto_now_add=True)
        lat = FloatField()
        lon = FloatField()
        name = TextField(blank=True)
        updated_at = DateTimeField(auto_now=True)
    
  • Make and run migrations:
    $ ./manage.py makemigrations
    $ ./manage.py migrate
    

GraphQL all the things

(Still in the travelog-api directory with virtualenv activated)

  • Create a new file travelog-api/geo/schema.py:
    import graphene
    from graphene_django.types import DjangoObjectType
    from .models import Location
    
    class LocationType(DjangoObjectType):
        class Meta:
            model = Location
    
    class Query(object):
        all_locations = graphene.List(LocationType)
    
        def resolve_all_locations(self, info, **kwargs):
            return Location.objects.all()
    
    class CreateLocation(graphene.Mutation):
        location = graphene.Field(LocationType)
    
        class Arguments:
            lat = graphene.Float()
            lon = graphene.Float()
            name = graphene.String()
    
        def mutate(self, info, lat, lon, name):
            loc = Location(lat=lat, lon=lon, name=name)
            loc.save()
            return CreateLocation(location=loc)
    
    class Mutation(graphene.ObjectType):
        create_location = CreateLocation.Field()
    
  • Create a new file travelog-api/travelog_api/schema.py:
    import graphene
    import geo.schema
    
    class Query(geo.schema.Query, graphene.ObjectType):
        pass
    
    class Mutation(geo.schema.Mutation, graphene.ObjectType):
        pass
    
    schema = graphene.Schema(query=Query, mutation=Mutation)
    
  • Edit travelog-api/travelog_api/urls.py:
    from django.contrib import admin
    from django.urls import path
    from graphene_django.views import GraphQLView
    from .schema import schema
    
    urlpatterns = [
        path("admin/", admin.site.urls),
        path("graphql/", GraphQLView.as_view(graphiql=True, schema=schema)),
    ]
    

Try it using the GraphiQL explorer

(Still in the travelog-api directory with virtualenv activated)

  • Run the server:
    $ ./manage.py runserver
    
  • Go to http://localhost:8000/graphql/ and you should see the GraphiQL interactive GraphQL explorer.
  • Create a location. Enter this mutation in the left pane and hit CTRL+ENTER:
    mutation {
      createLocation(name: "my first location", lat: 1, lon: 2) {
        location {
          id
        }
      }
    }
    See the response:
    {
      "data": {
        "createLocation": {
          "location": {
            "id": "1"
          }
        }
      }
    }
  • Query all locations:
    query {
      allLocations {
        createdAt
        id
        lat
        lon
        name
      }
    }
    See the response:
    {
      "data": {
        "allLocations": [
          {
            "createdAt": "2019-02-22T06:39:08.512197+00:00",
            "id": "1",
            "lat": 1,
            "lon": 2,
            "name": "my first location"
          }
        ]
      }
    }

References / See also

New Mac setup notes 2019

install Homebrew

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

brew cask install stuff

$ brew cask install iterm2
$ brew cask install emacs
$ brew cask install kdiff3

brew install stuff

$ brew install bat
$ brew install exa
$ brew install fzf
$ brew install glances
$ brew install gnupg
$ brew install icdiff
$ brew install markdown
$ brew install node@10
$ brew install postgresql
$ brew install python
$ brew install rbenv
$ brew install readline
$ brew install ripgrep
$ brew install screen
$ brew install tldr

copy SSH keys

set up homedir

$ cd /tmp
$ git clone [email protected]:saltycrane/homedir.git
$ rsync -avz homedir/ ~/

set up spacemacs

See https://github.com/saltycrane/.spacemacs.d#usage

install stuff via websites

misc stuff

  • fix the Meta/Alt/Option key in iTerm
    iTerm2 > Preferences > Profiles > Keys > Change Option key to "Esc+"
  • set a directory for screenshots
    $ defaults write com.apple.screencapture location /Users/eliot/Pictures 
    
  • allow gpg decrypting in emacs
    $ brew install pinentry-mac
    $ echo "pinentry-program /usr/local/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
    
  • python2 and pycrypto setup
    $ brew install python2
    $ pip install virtualenv
    $ brew install gmp # for pycrypto
    

see also

How to run PostgreSQL in Docker on Mac (for local development)

These are my notes for running Postgres in a Docker container for use with a local Django or Rails development server running on the host machine (not in Docker). Running in Docker allows keeping my database environment isolated from the rest of my system and allows running multiple versions and instances. (I previously had a problem where Homebrew upgraded Postgres when I didn't expect it to and my existing database became incompatible. Admittedly, I didn't know Homebrew well, but it was frustrating.) Disadvantages of Docker are it's another layer of abstraction to learn and interact with. We use Docker extensively at work, so from a mental overhead point of view, it's something I wanted to learn anyways. Currently I use the Homebrew Postgres for work, and Postgres in Docker for personal projects. I also wrote some notes on Postgres and Homebrew here.

Install Docker

Install Docker for Mac: https://docs.docker.com/docker-for-mac/install/.

Alternatively, you can install Docker using Homebrew: brew install homebrew/cask/docker

OPTION 1: Run Postgres using a single Docker command

Run a postgres container
$ docker run -d --name my_postgres -v my_dbdata:/var/lib/postgresql/data -p 54320:5432 -e POSTGRES_PASSWORD=my_password postgres:13

OPTION 2: Run Postgres using Docker Compose

Create a docker-compose.yml file
$ mkdir /tmp/myproject
$ cd /tmp/myproject

Create a new file docker-compose.yml:

version: "3"
services:
  db:
    image: "postgres:13"
    container_name: "my_postgres"
    environment:
      POSTGRES_PASSWORD: "my_password"
    ports:
      - "54320:5432"
    volumes:
      - my_dbdata:/var/lib/postgresql/data
volumes:
  my_dbdata:
Start Postgres

Pull the postgres image from hub.docker.com, create a container named "my_postgres", and start it in the background:

$ docker-compose up -d

See that it's working

See the logs:

$ docker logs -f my_postgres

Try running psql:

$ docker exec -it my_postgres psql -U postgres

hit CTRL+D to exit

For other commands such as starting, stopping, listing or deleting, see my Docker cheat sheet.

Create a database

$ docker exec -it my_postgres psql -U postgres -c "create database my_database"

Connect using Python and psycopg2

$ python3 -m venv myenv
$ source myenv/bin/activate
$ pip install psycopg2-binary

Create a new file named myscript.py

import psycopg2

conn = psycopg2.connect(
    host='localhost',
    port=54320,
    dbname='my_database',
    user='postgres',
    password='my_password',
)
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS test (id serial PRIMARY KEY, num integer, data varchar);")
cur.execute("INSERT INTO test (num, data) VALUES (%s, %s)", (100, "abcdef"))
cur.execute("SELECT * FROM test;")
result = cur.fetchone()
print(result)
conn.commit()
cur.close()
conn.close()

Run it

$ python myscript.py
(1, 100, 'abcdef')

Errors

  • docker: Error response from daemon: Conflict. The container name "/my_postgres" is already in use by container "b27594a414db369ec4876a07021c9ea738a55b3bc0a3ad5117158367131b99a2". You have to remove (or rename) that container to be able to reuse that name.

    If you get the above error, you can remove the container by running:

    $ docker rm my_postgres
    
  • Error response from daemon: You cannot remove a running container 7e94d205b6f4ef40ff885987f11e825e94eddbcd061481e591e07c87ed7cf86e. Stop the container before attempting removal or force remove

    If you get the above error, you can stop the container by running:

    $ docker stop my_postgres
    

See also

Magit in Spacemacs (evil-magit) notes

Magit and Org are two killer apps for Emacs. Here are my Magit notes using (the also excellent) Spacemacs (which uses evil-magit).

Contents

Show git status

  • SPC g s show Magit status view

Show help

  • SPC g s show Magit status view
  • ? get help

Show git log

  • SPC g s show Magit status view
  • l l show log view

Show all commits for the current file 1

  • SPC g f l show git log for the current file

Diff a range of commits

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor on a commit
  • V to select the line
  • use j and k to position the cursor on another commit
  • d r to show a diff of the range of commits

Checkout a local branch

  • SPC g s show Magit status view
  • b b checkout a branch
  • select or enter the branch name and hit ENTER

Checkout a commit

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor on a commit
  • b b ENTER to checkout that commit

Checkout a different revision of a file

  • SPC g s show Magit status view
  • l l show log view
  • move point to the commit you want to checkout (using j and k)
  • O (capital letter O) f reset a file
  • hit ENTER to select the default revision selected above. (it will look something like master~4)
  • select a file
  • q to close the log view and see the file at the selected revision is staged

Open a different revision of a file 8

  • SPC g s show Magit status view
  • l l show log view
  • move point to the commit you want to checkout (using j and k)
  • SPC g f F (magit-find-file) to open a file at a revision
  • ENTER to use the selected commit
  • select the name of the file to open

Create a local branch from a remote branch

  • SPC g s show Magit status view
  • b c create a branch
  • select or enter the remote branch and hit ENTER
  • hit ENTER to use the same name or enter a new name and hit ENTER

Pull from upstream

  • SPC g s show Magit status view
  • F u pull from upstream

Push to upstream

  • SPC g s show Magit status view
  • P u push to upstream

Stage files and commit

  • SPC g s show Magit status view
  • use j and k to position the cursor on a file
  • TAB to show and hide the diff for the file
  • s to stage a file (u to unstage a file and x to discard changes to a file)
  • c c to commit
  • write a commit message and save with SPC f s
  • , c to finish the commit message

Stage specific hunks 2

  • SPC g s show Magit status view
  • M-n / M-p to move to the "Unstaged changes" section
  • j / k to move to the desired file
  • TAB to expand the hunks in the file
  • M-n / M-p to move to different hunks
  • s / u to stage or unstange hunks
  • x to discard a hunk
  • c c to commit
  • Enter a commit message and save with SPC f s
  • , c to finish the commit

Merge master into the current branch

  • SPC g s show Magit status view
  • m m merge
  • select or enter master and hit ENTER

Rebase the current branch onto master

  • SPC g s show Magit status view
  • r e rebase
  • select or enter master and hit ENTER

Use interactive rebase to squash commits

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor on a commit
  • r i to start the interactive rebase
  • use j and k to position the cursor on a commit to squash
  • s to mark the commit as to be squashed. (use s multiple times to squash multiple commits.)
  • , c to make it happen
  • edit the new squashed commit message and save with SPC f s
  • , c to finish

Use interactive rebase to reorder commits

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor on a commit
  • ri to start the interactive rebase
  • use j and k to position the cursor on a commit to reorder
  • use M-k or M-j to move the commit up or down
  • , c to make it happen

Revert a commit

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor on the commit you want to revert
  • _ O (capital letter O) to revert the commit
  • edit the commit message and save with SPC f s
  • , c to finish

(Soft) reset the last commit

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor one commit before the last one
  • O (capital letter O) s to soft reset
  • the selected commit should be e.g. master~1. Hit ENTER

Stash changes

  • SPC g s show Magit status view
  • z z stash changes
  • enter stash message and hit ENTER

Pop stash

  • SPC g s show Magit status view
  • z p pop from stash
  • select the stash to pop and hit ENTER

Copy git commit SHA 3

  • SPC g s show Magit status view
  • l l show log view
  • use j and k to position the cursor on a commit
  • y s copy the git commit SHA

Copy text from a Magit buffer 4

  • SPC g s show Magit status view
  • \ switch to text mode
  • copy text using normal vim keystrokes
  • \ switch back to Magit mode

Run a shell command 5

  • SPC g s show Magit status view
  • ! s run a shell command
  • enter a command to run and hit ENTER

List all branches 6

  • SPC g s show Magit status view
  • y r show refs

Jump to the next/prev section in the status view 7

  • SPC g s show Magit status view
  • g j jump to the next section
  • g k jump to the previous section

Create a worktree from an existing branch

  • SPC g s show Magit status view
  • ? % b create a worktree from an existing branch
  • select or enter the branch name and hit ENTER
  • enter the path for the worktree and hit ENTER

Switch to a worktree

  • SPC g s show Magit status view
  • ? % g visit worktree
  • select or enter the path of the worktree and hit ENTER

Delete a worktree

  • SPC g s show Magit status view
  • ? % k delete a worktree
  • select or enter the path of the worktree and hit ENTER

References

  1. https://twitter.com/iLemming/status/1058507342830923776
  2. https://github.com/emacs-evil/evil-magit
  3. https://twitter.com/a_simpson/status/749316494224265216
  4. https://twitter.com/iLemming/status/986074309234802688
  5. https://twitter.com/_wilfredh/status/689955624080248833
  6. https://emacs.stackexchange.com/a/27148
  7. https://www.youtube.com/watch?v=j-k-lkilbEs
  8. https://emacs.stackexchange.com/questions/7655/how-can-i-open-a-specific-revision-of-a-file-with-magit/7683#7683