npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@surglogs/with-derived-props

v1.1.0

Published

withDerivedProps HOC for easy memoization of derived data in React components

Downloads

732

Readme

withDerivedProps

Build Status

withDerivedProps is a Higher Order Component that allows to derive new data from existing props in a succint manner and memoize them automatically. If it reminds you of withPropsOnChange HOC from recompose, you are right - we based this HOC on it. We adjusted the API for most common usecases to be easier and safer to use. We have also written guidelines that tell you when and why to use it.

Instalation

npm i @surglogs/with-derived-props recompose

What is this library good for?

Since this library is basically an alternative to reselect library, our examples are based on the reselect examples, so that you can compare them visually.

Todo list example

Let's say we have a list of todos stored in the redux store and now we need to filter the todos by their state (complete or active) and show them to the user.

Our first try might look something like this: (to see the whole code please look here)

import { connect } from 'react-redux'
import { compose } from 'recompose'

import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = state => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter),
  }
}

const VisibleTodoList = compose(
  connect(
    mapStateToProps,
    { toggleTodo },
  ),
)(TodoList)

export default VisibleTodoList

Looks good! We got the todos and visibilityFilter from the redux store and computed filtered todos using getVisibleTodos function. So what is the problem then?

The problem

Unfortunately, it is not very efficient and might slow down your app significantly. The problem is that mapStateToProps is called every time redux state changes (even if nothing related to your component really changed). Therefore you cannot do anything complex in your mapStateToProps. In our example, we compute the visible todos everytime the state changes. If we have a lot of todos (which is absolutely possible in real app) our app might become very slow because of this.

Another problem is that every time we compute getVisibleTodos, we create a new array and therefore also a new reference. Because of that, we cannot effectively use optimizations pure or memo which are the basic techniques to eliminate wasted renders and speed up your app.

Looks bad 😢. What can we do with it?

Reselect

If you use redux, you probably already heard about (or even better used) this library. It allows you to memoize our getVisibleTodos which removes the complexity from mapStateToProps and also removes the problem with creating new references. The memoized functions are called selectors.

You can take a look at reselect's solution to our problem.

Well, why shouldn't you use reselect right?

It has serious problems. If you try to reuse the same selector in two different components, the memoization stops to work correctly and the optimizations are gone. This is very unpleasant since this problem is very hard to debug. Reselect provides a solution how to prevent this, however it requires tons of boilerplate... which is totally unnecessary because we have a better solution ;).

withDerivedProps to the rescue

First, we get the stuff we need to compute the filtered todos:

const mapStateToProps = state => {
  return {
    todos: state.todos,
    visibilityFilter: state.visibilityFilter,
  }
}

Pretty trivial.

Notice we are not creating here any new data here, we merely pull existing data from the store.

Next we compute the filtered data in withDerivedProps HOC. As first argument we provide keys of props we need to compute the data ['todos', 'visibilityFilter']. Next we provide an object with properties we want to compute and pass the function using which the data is computed { filteredTodos: ({ todos, visibilityFilter }) => { /* compute filtered todos */ } }.

import withDerivedProps from 'withDerivedProps'

withDerivedProps(['todos', 'visibilityFilter'], {
  filteredTodos: ({ todos, visibilityFilter }) => {
    switch (filter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  },
})

Generally, the withDerivedProps structure looks like this:

withDerivedProps(keys, {
  [computedProp]: computationFunction,
})

Finally we compose the connect and withDerivedProps HOCs using compose function from recompose:

import { connect } from 'react-redux'
import { compose } from 'recompose'
import withDerivedProps from 'withDerivedProps'

import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const mapStateToProps = state => {
  return {
    todos: state.todos,
    visibilityFilter: state.visibilityFilter,
  }
}

const VisibleTodoList = compose(
  connect(
    mapStateToProps,
    { toggleTodo },
  ),
  withDerivedProps(['todos', 'visibilityFilter'], {
    filteredTodos: ({ todos, visibilityFilter }) => {
      switch (filter) {
        case 'SHOW_ALL':
          return todos
        case 'SHOW_COMPLETED':
          return todos.filter(t => t.completed)
        case 'SHOW_ACTIVE':
          return todos.filter(t => !t.completed)
      }
    },
  }),
)(TodoList)

export default VisibleTodoList

Tada 🎉 we are done. How does it work? Every time new props come to withDerivedProps, the HOC checks if any relevant prop (todos or visibilityFilter) changed. If it didn't, the HOC does nothing. If something relevant did change, the HOC recomputes the filteredTodos using the function we provided.

If we want to make the code more readable we can extract the computation function somewhere else. It is a good practise to prefix the function name with derive but it is really up to you.

const deriveFilteredTodos = ({ todos, visibilityFilter }) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

withDerivedProps(['todos', 'visibilityFilter'], {
  filteredTodos: deriveFilteredTodos,
})

Now you can notice the key difference between reselect and withDerivedProps approach. withDerivedProps does the memoization in the component whereas reselect binds it to the computation function. That's why withDerivedProps has no problem with reusing the same computation function in multiple components as reselect does.

Note that your computation functions receive only the props that you listed (['todos', 'visibilityFilter']). That's a good thing, because if you received all the props in the computation function, you might forget to list it in the keys and the data might not recompute when it should. If you want to receive all the props, you can provide third optional argument { passAllProps: true }. However, DON'T DO THIS IF YOU ARE NOT SURE WHAT YOU ARE DOING! ;).

Deriving multiple props at once

If you want to derive several props from the props, you can either use multiple withDerivedProps usages in row:

compose(
  connect(...),
  withDerivedProps(...),
  withDerivedProps(...),
  withDerivedProps(...)
)

or you can put them in single withDeriveProps:

withDerivedProps(['todos', 'visibilityFilter'], {
  filteredTodos: ({ todos, visibilityFilter }) => {
    switch (filter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  },
  shouldShowEmptyLabel: ({ filteredTodos }) => filteredTodos.length === 0
})

Please note that the first approach is more flexible and might optimize the component better, especially if the props are computed from many different props.

withDerivedProp

For derivations that depend only on one prop, you can you use simpler version of our HOC withDerivedProp:

import { withDerivedProp } from 'withDerivedProps'

withDerivedProp('todos', todos => todos.filter(t => !t.completed), { target: 'visibleTodos' })

If you don't want to create a prop with name but want to overwrite the existing one, you can omit the third argument:

withDerivedProp('todos', todos => todos.filter(t => !t.completed))

Frequently asked questions

What if I want use the computation function outside the component (e.g. while calling API)? I guess it will be slow because the function itself is not memoized!

Generally, no it won't be slow, you don't have to worry. It is because during the API call (or similar action) you only need to compute the data once unlike in component, where it can happen dozens times per second.

However, even in the component itself it wouldn't be as bad to recompute the data often. The main problem is that recomputation creates new references which cause rerenders. And rerenders (or React reconciliation) can be very very time expensive (especially in long lists). That's the main reason, why we need to get rid of redundant data recomputations.