Tuesday, April 23, 2019

Refreshing Next.js Page Data

Something that I struggled wrapping my head around when learning Next.js was refreshing data on a given page. It seemed that the only option was to trigger the browser to reload the page, which isn't at all what I was going for. Next.js does a fantastic job of populating the page with data, whether it's rendered on the server or in the browser. The initial page data and the plumbing needed to fetch it is something I just don't need to think about.

In this post, I'll walk through my stumbling points for refreshing page data and the approach that I took to get around the issue. Let's start off by looking at a trivial page in a Next.js app:

async function getInitialProps() {
  const response = await fetch('/api/some/endpoint');
  const myFetchedData = await response.json();

  return { myFetchedData };
}

export default function MyPage(props) {
  return <strong>{props.myFetchedData}</strong>;
}

MyPage.getInitialProps = getInitialProps;

The getInitialProps() function fetches the data that our page needs. It makes a fetch() call to some imaginary API endpoint, gets the JSON response by calling response.json(), and returns an object with a myFetchedData property. The object that is returned is passed to the MyPage component as props. You can see that our component uses the props.myFetchedData value when rendering its content. Finally, the getInitialProps() function is assigned to MyPage.getInitialProps. This is how Next.js knows how to populate our page data. It will call this function on the server if this page is requested, or in the browser if this page is navigated to after the initial load.

At this point, we have a page that loads data on initial render. But what happens if the data returned by the API endpoint has changed somehow? For example, some other component in our application changes this value or creates a new resource, etc. There are any number of reasons that you might want to refresh the data that's passed to MyPage as properties. This is where I got stuck.

To illustrate the issue, let's add a refresh button to our page that simply calls the getInitialProps() function, thus calling the API again and getting updated data:

async function getInitialProps() {
  const response = await fetch('/api/some/endpoint');
  const myFetchedData = await response.json();

  return { myFetchedData };
}

export default function MyPage(props) {
  return (
    <main>
      <strong>{props.myFetchedData}</strong>
      <button onclick={getInitialProps}>Refresh</button>
    </main>
  );
}

MyPage.getInitialProps = getInitialProps;

The onClick handler of the new refresh button gets us partway there. It makes the API call, which responds with updated data, but then it doesn't do anything with it. At this point, I realized that if your Next.js page has data that needs to be refreshed, it needs to be stateful. Thankfully, you can still use the getInitialProps mechanism of Next.js to handle data fetching for the initial page load. Here's what the pattern looks like:

import { useState } from 'react';

async function fetchData() {
  const response = await fetch('/api/some/endpoint');
  const myFetchedData = await response.json();

  return { myFetchedData };
}

export default function MyPage(props) {
  const [
    myFetchedData,
    setMyFetchedData
  ] = useState(props.myFetchedData);

  async function refresh() {
    const refreshedProps = await fetchData();
    setMyFetchedData(refreshedProps.myFetchedData);
  }

  return (
    <main>
      <strong>{myFetchedData}</strong>
      <button onclick="{refresh}">Refresh</button>
    </main>
  );
}

MyPage.getInitialProps = fetchData;

The MyPage component now has myFetchedData as state. The original myFetchedData property is still passed to the component because this is how the Next.js mechanism for data fetching passes data to our components. However, the useState() hook uses the myFetchedData property value as default state value for the myFetchedValue state. This means that you don't need to worry about the myFetchedValue property - just use the myFetchedValue state instead since it has the same value.

Now that we've setup myFetchedValue to be stateful, we can implement a proper refresh function. But first, take note that we've renamed the getInitialProps() function to fetchData() because it's now more generic than fetching the initial page data. The refresh() function calls fetchData(), then calls setMyFetchedData() to update the state and re-render the page.

To summarize how state can help you with refreshing data used by your Next.js pages, just remember:

  1. The default value passed to useState() is the value that is fetched initially by Next.js.
  2. You can reuse your data fetching function any time you need fresh data. Just update the state when you have the new data.

Monday, August 28, 2017

Lodash Logic

If you're using Lodash, you have a handful of tools at your disposal for organizing logic into functions. This is a nice alternative to imperative if statements sprinkled throughout your code. For example, let's say that you only want to execute code if one of several choices is true. You could use the some() function as follows:

const choices = [
  { name: 'choice 1', value: true },
  { name: 'choice 2', value: true },
  { name: 'choice 3', value: false }
];

if (_(choices).map('value').some()) {
  console.log('true');
else {
  console.log('false');
}

The choices array represents the choices that we have available. The name property isn't actually used for anything in this code. The value property is what we're interested in here. We want to run some code if any value is true.

To do so, we're using an if statement. With the help of the map() and some() Lodash functions, we can easily check for this condition. In this case, the statement evaluates to true because there's two true values in the array. We can also check to make sure that every value is true before executing a piece of code:

if (_(choices).map('value').every()) {
  console.log('true');
else {
  console.log('false');
}

In this case, the else path is followed because not every value is true.

The _.some() and _.every() functions are helpful with simplifying the conditions evaluated by if statements. For example, _.some() provides the same result as chaining together a bunch of logical or (||) operators. Likewise, _.every() replaces logical and (&&) operators.

The result is that instead of having to maintain the condition that's evaluated in the if statement, we can simply add new values to the choices collection. Essentially, this is a step toward declarative programming, away from imperative programming.

Let's think about the if statement used above, and what it's actually doing. It's calling console.log() when some condition is true, and it's calling console.log() again when the condition is false. The problem with if statements like this is that they're not very portable. It'd be much easier to call a function with the possible choices as an argument, and the correct behavior is invoked.

Here's what this might look like:

const some = (yes, no) => (...values) =>
  new Map([
    [true, yes],
    [false, no]
  ]).get(_.some(values))();

Let's break this code down:
  • We've created a higher-order function called some() that returns a new function.
  • The returned function accepts an arbitrary number of values. These are tested with Lodash's _.some().
  • The some() function accepts yes() and no() functions to run based on the result of calling _.some(values)
  • A Map is used in place of an if statement to call the appropriate logging function.
With this utility, we can now compose our own functions that values as arguments, and based on those arguments, run the appropriate function. Let's compose a function using some():

const hasSome = some(
  () => console.log('has some'),
  () => console.log('nope')
);

Now we have a hasSome() function will log either "has some" or "nope", depending on what values are passed to it:

hasSome(0, 0, 0, 1, 0);
// -> has some
hasSome(0, 0, 0, 0);
// -> nope

Now any time that you want an if statement that evaluates simple boolean expressions and runs one piece of code or another, depending on the result, you can use some() to compose a new function. You then call this new function with the choices as the arguments.

Let's create an every() function now that works the same way as some() except that it tests that every value is true:

const every = (yes, no) => (...values) =>
  new Map([
    [true, yes],
    [false, no]
  ]).get(_.every(values))();

The only difference between every() and some() is that we're using _.every() instead of _.some(). The approach is identical: supply yes() and no() functions to call depending on result of _.every().

Now we can compose a hasEvery() function, just like we did with the hasSome() function:

const hasEvery = every(
  () => console.log('has every'),
  () => console.log('nope')
);

Once again, we've avoided imperative if statements in favor of functions. Now we can call hasEvery() from anywhere, and pass in some values to check:

hasEvery(1, 1, 1, 1, 1)
// -> has every
hasEvery(1, 1, 1, 1, 0)
// -> nope

Lodash has a _.cond() function that works similarly to our Map approach in some() and every(), only more powerful. Instead of mapping static values, such as true and false, to functions to run, it maps functions to functions. This allows you to compute values to test on-the-fly.

Before we get too fancy, let's rewrite our some() and every() functions using _.cond():

const some = (yes, no) => _.flow(
  _.rest(_.some, 0),
  _.cond([
    [_.partial(_.eq, true), yes],
    [_.stubTrue, no]
  ])
);

Let's break this down:
  • The _.flow() function creates a new function by calling the first function, then passing it's return value to the next function, and so on.
  • The _.rest() function creates a new function that passes argument values as an array to it's wrapped function. We're doing this with _.some() because it expects an array, but we just want to be able to pass it argument values instead.
  • The _.cond() function takes an array of pairs. A pair is a condition function, and a function to call if the condition function returns true. The first pair that evaluates to true is run, and no other pairs are evaluated.
  • The _.partial(_.eq, true) call makes a new function that tests the output of _.some().
  • The _.stubTrue() function will always evaluate to true, unless something above it evaluates to true first. Think of this as the else in an if statement.
We can use this new implementation of some() to compose the same hasSome() function that we created earlier and it will work the same way. Likewise, we can implement the every() function using the same approach.

For something as simple as the some() and every() functions, the _.cond() approach doesn't present any clear advantage over the Map approach. This is because there are exactly two paths. Either the condition evaluates to true, or it doesn't. Often, we're not working with simple yes/no logical conditions. Rather, there are a number of potential paths.

Think of this as a an if-else statement with lots of conditions. Suppose we had the following conditions:

const condition1 = false;
const condition2 = true;
const condition3 = false;

Instead of a simple yes/no question with two potential paths, now we have 3. Later on, we might have 4, and so on. This is how software grows to be complex. Here's how we would evaluate these conditions and execute corresponding code using _.cond():

const doStuff = _.cond([
  [_.constant(condition1), () => console.log('Condition 1')],
  [_.constant(condition2), () => console.log('Condition 2')], 
  [_.constant(condition3), () => console.log('Condition 3')]
]);

doStuff();
// -> Condition 2

For each of the condition constants that we defined above, we're using the _.constant() function in _.cond(). This creates a function that just returns the argument that is passed to it. As you can see, console.log('Condition 2') is called because the function returned by _.constant(condition2) returns true.

It's easy to add new pairs to _.cond() as the need arises. You can have 20 different execution paths, and it's just as easy to maintain as 2 paths.

In this example, we're using static values as our conditions. This means that doStuff() will always follow the same path, which kind of defeats the purpose of this type of code. Instead, we want the path chosen by _.cond() to reflect the current state of the app:

const app = {
  condition1: false,
  condition2: false,
  condition3: true
};

Instead of using _.const(), we'll have to somehow pass the app into each evaluator function in _.cond():

const doStuff = _.cond([
  [_.property('condition1'), () => console.log('Condition 1')], 
  [_.property('condition2'), () => console.log('Condition 2')], 
  [_.property('condition3'), () => console.log('Condition 3')]
]);

The _.property() function creates a new function that returns the given property value of an argument. This is where the _.cond() approach really shines: we can pass arguments to the function that it creates. Here, we want to pass it the app object so that we can process its state:

doStuff(app);
// -> Condition 3

app.condition1 = true;
app.condition3 = false;

doStuff(app);
// -> Condition 1

When doStuff() is called the first time, the console.log('Condition 3') path is executed. Then, we change the state of app so that condition1 is true and condition3 is false. When doStuff() is called again with app as the argument, the console.log('Condition 1') path is executed.

So far, we've been composing functions that use console.log() to print values. If you write smaller functions that return values instead of simply printing them, you can combine them to build more complex logic. Think of this as an alternative to implementing nested if statements.

As an example, suppose we have the following two functions:

const cond1 = _.cond([
  [_.partial(_.eq, 1), _.constant('got 1')],
  [_.partial(_.eq, 2), _.constant('got 2')]
]);

const cond2 = _.cond([
  [_.partial(_.eq, 'one'), _.constant('got one')], 
  [_.partial(_.eq, 'two'), _.constant('got two')]
]);

These functions themselves follow the same implementation approach, using _.cond(). For example, cond1() will return the string 'got 1' or 'got 2', depending on the number supplied as an argument. Likewise, cond2() will return 'got one' or 'got two', depending on the string argument value.

While we can use both of these functions on their own, we can also use them to compose another function. For example, we could write an if statement that would determine which one of these functions to call:

if (_.isFinite(val)) {
  cond1(val);
} else if (_.isString(val)) {
  cond2(val);
}

Remember, this approach isn't very portable. To make it portable, in the sense that we don't have to write the same if statement all over the place, we could wrap the whole thing in a function. Or, we could just use _.cond() to compose it:

const cond3 = _.cond([
  [_.isFinite, cond1],
  [_.isString, cond2]
]);

cond3(1);
// -> "got 1"
cond3(2);
// -> "got 2"
cond3('one');
// -> "got one"
cond3('two');
// -> "got two"

Using _.cond(), you can compose complex logic by reusing existing functions. This means that you can keep using these smaller functions where they're needed, and you can use them as pieces of larger functions.

Monday, May 29, 2017

Learning jQuery, Fifth Edition

I'm pleased to announce the availability of Learning jQuery 3. This is the fifth edition of the book, and given its track record, I didn't want to diverge from what has worked so well over all these years.

Tuesday, May 17, 2016

Flux Architecture

Flux is quickly becoming the standard architecture for building large-scale applications. It's the topic of my latest book, Flux Architecture, available on Amazon.