DEV Community

Cover image for React lists without .map

React lists without .map

Mike Talbot ⭐ on August 04, 2021

When we are rendering data in React we often grab an array and do a .map() to write out our interface. The inclusion of instructional statements i...
Collapse
 
dikamilo profile image
dikamilo • Edited

React lists without map and still uses map but hidden deep inside after refactoring ;)

You repeat list is still rerendered as whole when you add or remove elements. You can see it in devtools react profiler.

Each RenderItem is rerendered because remove prop change every time, you can fix it by wrapping remove function with useCallback. But RenderItem will still be rerendered in other cases because parent compontent rerenders each time. This can be fixed by using memo on RenderItem and removing index prop from item.type because it changes every time and causes rerenders on children component.

After this, adding new element will render only one RenderItem and removing item will just remove single component.

Cheers

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Well you don't use a .map when you make this list was my point ;)

Good point on the remove - I'd do that by using a useCallback if I'd remembered lol:

function App4() {
    const [render, setRender] = useState(items)
    const remove = useCallback(_remove, [])
    return (
        <Box>
            <List className="App">
                <Repeat list={render}>
                    <RenderItem remove={remove} />
                </Repeat>
            </List>
            <Button variant="contained" color="primary" onClick={add}>
                Add
            </Button>
        </Box>
    )

    function add() {
        setRender((items) => [
            { name: "Made up at " + Date.now(), on: false }
            ...items,
        ])
    }

    function _remove(item) {
        setRender((items) => items.filter((i) => i !== item))
    }
}
Enter fullscreen mode Exit fullscreen mode

The index property is important for many things that require sorting etc and while the component will be called when you update the list it then shouldn't be creating a new element if the index stays stable (as I'm inserting at the top of the list that isn't likely)

Collapse
 
jaivinwylde profile image
Jaivin Wylde

I noticed you set the length of the array. What's the time complexity of doing that to truncate an array? Is it O(1) and the extra elements get garbage collected at a later time?

Thanks!

Collapse
 
miketalbot profile image
Mike Talbot ⭐

A very good question, I researched other people's array truncation jsperfs and tried a couple of my own, it ended up being the fastest way at the time to truncate. I'd expect the V8 engine considers the out of range elements to be unreachable and available for GC but possibly doesn't consider the pointer array to be resized - it's a guess though, I suppose you'd have to look at the implementation detail in the engine to be sure and I didn't do that.

Collapse
 
jaivinwylde profile image
Jaivin Wylde

One more thought on this I had was if the garbage collector needs to iterate over each extra element to remove it from memory or could it somehow do a bulk delete of the extra elements? If it has to iterate then it would be the same as array.slice with O(n - m) where n = previous length, m = new shorter length. But if it can do a bulk delete it would be good. Not sure though as I don't know this much about the GC, do you have more info or could point me in the right direction?

Collapse
 
jaivinwylde profile image
Jaivin Wylde

appreciate the response thanks!

Collapse
 
discworldza profile image
DiscworldZA

In theory this is great. It addresses a problem in React I hated for a very long time. However in practise this is too complex I feel. What you did was great and the first step is very important however the second step is too complex. Try having a Junior understand that without needing to explain it. The cons of complexity far outweigh the pros of micro optimisation here. This also turns JSX in React into a templating language which is something React is not made for. Not because it isn't a good idea but because it isn't the convention and therefore requires training. Again this is a great concept but too complex for production I feel.

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

I do see your point and I think the code you use on a project should be balanced by the architects controlling it.

Our main app uses this and a <VirtualRepeat> that has the same signature but lets you specify a number of visible items and it's nice because everyone knows them and can swap between them as necessary - however - it does require training and would not work well in many circumstances. Also we use the version that does <Repeat list={blah} item={<SomeItem blahblah={doSomething}/>} /> over the one that uses it as a child.

Collapse
 
ianwijma profile image
Ian Wijma

Personally I'd prefer using normal JavaScript instead of using a magical wrapper that does exactly the same as a simple .map.

I do build wrappers where it makes sense, but often I'd rather use the following strategy to clean up my code.

Here is a simple example, where I go from a messy implementation:

To a more cleaner implementation:

Collapse
 
vladislavmurashchenko profile image
VladislavMurashchenko

In my opinion, first is easier to read because of not so many abstractions. I'd prefer to create extra abstractions only when implementation becomes big. Then it becomes simpler to understand which parts of code are worth separate abstractions and which are not.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Agreed, when it's small, totally. I see them bigger than the surrounding component, then I think it should be separated.

Collapse
 
jwhenry3 profile image
Justin Henry

I love abstractions, but only when it reduces boilerplate and effort to create. The amount of code generated in order to abstract away map makes the effort not that valuable. I do say it's a good learning exercise, but I wouldn't do this in all projects since you introduce more components into the reconciler, and you're needing to spread a bunch of props in order to get the desired result, which means more overhead.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Maybe I'm missing something, but isn't the effect only adding one parent component? All the others would need to be added anyway and have their own props object returned by the createElement(). I've just moved the props setting (and admittedly probably created one more object along the way).

If you wanted to be pure and just make the one props object that every entry in the map needs you could write the output like this:

function Repeat({
    list,
    children,
    item = children.type ? children : <Simple />,
    pass = "item",
    keyFn = getKey
}) {
    return list.map((iterated, index) => {
        return {type: item.type, key: keyFn(iterated), props: {...item.props, [pass]: iterated, index}
    })
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vladislavmurashchenko profile image
VladislavMurashchenko • Edited

In general, doing something like this is quite bold. I respect you for it.

But there are quite many disadvantages in this approach. There are some:

Props like item and index are passed to components implicitly. I personally prefer when everything is explicit. Also in most cases we don't need index and this index can cause unnecessary updates in some cases (for example inserting item to the middle). It can be fixed by get rid of cloning react elements and using render functions.

Also, this cloning React elements with spread operator is quite going too deep into react internal implementation. It would be better to use clone element

I noticed, that getKey relay on references of objects. That means that when reference changes, then key also changes. When key changes, component will be unmounted and mounted again. So it would be better just to have keyFn as required argument.

Repeat can be used only with react elements which doesn't have children inside them.

The last example with so many mutations inside looks really dangerous

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Sure, I get your concerns.

A short rebuttal:

  • Repeat can be used on items with inner components, children is just another prop and it works fine

  • Index, sure can go any way on this, I mostly use this on lists that allow sorting, and the sortable HOC requires index so I just leave it in there. You could easily make it work differently by having explicit props for sure.

  • getKey is just a default implementation that works for object items that don't change, you can supply another keyFn and in my main app this would always be v=>v.id || v._id - but this is of course implementation specific.

Collapse
 
vladislavmurashchenko profile image
VladislavMurashchenko • Edited

About children I just mean, that it is not possible to do something like this:

{items.map(item => (
  <div key={item.name}>
      <span>{item.name}</span>
      <span>{item.age}</span>
  </div>
))}
Enter fullscreen mode Exit fullscreen mode

Because with Repeat we don't have access to item itself. All item based rendering always have to be inside a component and external children of the component must not rely on the item.

In some cases we just don't need a component

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

Right, right sorry. I should have though that through :)

Yeah you can't do that, unless you use a version which takes a function as a child, but then if it's this case... well I'd just probably do the items.map as you point out. My project's version does have support for a function child but really the semantics are then using { } so I don't bother and just use the map.

The real point was when you need to do a sub component because you need hooks in the wrapper. I'd prefer this laid out in the template version (because frequently in our code base we'd swap from <Repeat> to <VirtualRepeat> which has the same signature, but virtualises the elements.

Collapse
 
thekashey profile image
Anton Korzunov

‘Repeat’ is a good abstraction, which hides some complexity behind it and increases signal/noise ratio.
A perfect example of DSL in the form of Component 👍

Having configurable getKey function with automated defaults is 🤟 as well, because those WeakMap Magic’s are not required if you a real key to use.

in idea to make WeakMap solution a little more testable please take a look at react-uid

Collapse
 
joesky profile image
Joe_Sky

How about this approach? github.com/leebyron/react-loops

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

Thanks, not seen that one!

On first scan, it's using a render function for items - which is fine, but I think it still has cognitive dissonance as you reason with the parameters of the render function and figure out the key on the item. I'm in JSX, I'm about to write some more JSX, I don't need this render function is my personal preference.

My full implementation of this allows for that approach (and multiple functional children if you like!) but if I look at the code where we use our function, it never actually happens. All we ever use is the item={<SomeRenderer/>} because it covers off all of our requirements and the code scans better.

Repeat with auto keying and debug fall back to JSON is 29 lines of code. I wouldn't make an npm package out of that :) I'd write a Dev article LOL!

Collapse
 
joesky profile image
Joe_Sky

Oh, I basically understand, thanks for your detailed reply.

For the approach without render function, I also have seen this: github.com/AlexGilleran/jsx-contro...

But it has a trouble problem that in TS: the item needs to define a variable separately to match the TS type.

I feel that the approach of react-loops, which maybe has a higher recognition, because it uses the render props pattern commonly used in React. For example, solidjs also uses this <For> design: solidjs.com/docs/latest/api#%3Cfor%3E

After reading your article, I think it's certainly useful. I just feel that at first glance, there is a lot of code, which is a little complex for people who see it for the first time, perhaps it would be better to have a simplified version. Thanks for sharing~

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

I mean, you would have the actual function in a utilities file and it's only a few lines of code. It works fine in TS. Here's an example of Repeat written that way.

Collapse
 
globalkonvict profile image
Sarthak Dwivedi

Seriously that's cool, and I see you define function after return of component markup. I might try it work. Let's see what I get from my manager, lol.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Yeah that's a "thing", I always do this because I like the main purpose - the body in other words - of a routines to be first. Then utilities it uses next. Doesn't work in TS with linting (because TS hates me!)

Collapse
 
mrdulin profile image
official_dulin

Make things complicated. .map is clear. If your list item JSX has many, you should consider extract an item component.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

:) Your choice, but if you look at the next part which takes the principle to virtualised windows and lists then (as that article mentions) it becomes a lot more than sugar.

Collapse
 
mrvaa5eiym profile image
mrVAa5eiym

nice exercise but I do not think that the point of react is to write another framework with it :)

Cheers