2

I have a list of medias and my goal is to be able to show the currently playing media. Playlist

To do so, I compare the playing media ID with the one from the list to apply the correct style. My issue is that when clicking on another item, all items re-render because they have a dependency on the playing media which is observable.

class AppStore {
  ...
  get playingVideo() {
    if (!this.player.videoId || this.player.isStopped) {
      return null;
    }
    return this.videos[this.player.videoId];
  }
}
const DraggableMediaItem = observer(({ video, index }) => {
  const store = useAppStore();
  const isMediaActive = computed(
    () => store.playingVideo && video.id === store.playingVideo.id
  ).get();

  console.log("RENDER", video.id);

  const onMediaClicked = (media) => {
    if (!isMediaActive) {
      playerAPI.playMedia(media.id).catch(snackBarHandler(store));
      return;
    }
    playerAPI.pauseMedia().catch(snackBarHandler(store));
  };

  let activeMediaProps = {};
  if (isMediaActive) {
    activeMediaProps = {
      autoFocus: true,
      sx: { backgroundColor: "rgba(246,250,254,1)" },
    };
  }
  return (
    <Draggable draggableId={video.id} index={index}>
      {(provided, snapshot) => (
          <ListItem
            ref={provided.innerRef}
            {...provided.draggableProps}
            {...provided.dragHandleProps}
            style={getItemStyle(
              snapshot.isDragging,
              provided.draggableProps.style
            )}
            button
            disableRipple
            {...activeMediaProps}
            onClick={() => onMediaClicked(video)}
          >
            <Stack direction="column" spacing={1} sx={{ width: "100%" }}>
              <Stack direction="row" alignItems="center">
                <ListItemAvatar>
                  <MediaAvatar video={video} />
                </ListItemAvatar>
                <ListItemText primary={video.title} />
                <ListItemText
                  primary={durationToHMS(video.duration)}
                  sx={{
                    textAlign: "right",
                    minWidth: "max-content",
                    marginLeft: "8px",
                  }}
                />
              </Stack>
            </Stack>
          </ListItem>
      )}
    </Draggable>
  );
});

I thought making isMediaActive a computed value would prevent that, but since the value the computation is based on changes, it triggers an update.

Is it possible to only re-render when the computed value changes ?

[EDIT]

Following @danila's comment, I cleaned up my code and injected the isActive parameter. However, I must still be missing something, since the List doesn't re-render when the player's video changes.

That would be the current pseudocode:

const MediaItem = observer(({ isActive }) => {
  let activeMediaProps = {};
  if (isActive) {
    activeMediaProps = {
      sx: { backgroundColor: "rgba(246,250,254,1)" },
    };
  }

  return <ListItem {...activeMediaProps}> ... </ListItem>;
});

const Playlist = observer(() => {
  const store = useAppStore();
  const items = store.playlist;

  return (
    <List>
      {items.map((item) => (
        <MediaItem isActive={item.id === store.player.videoId} />
      ))}
    </List>
  );
});

[EDIT 2]

Code sandbox link with a working example:

https://codesandbox.io/s/silent-lake-2lvdc?file=/src/App.js

Thank you in advance for your help and time.

Danila
  • 15,606
  • 2
  • 35
  • 67
Luc
  • 164
  • 1
  • 10

1 Answers1

1

First of all you can't use computed like that. In most cases computed should be used like a property in your store. Similar to observable.

As for the question, if you don't want items to rerender you could provide this flag through props, something like that in pseudocode

const List = observer(() => {
  return (
    <div>
      {items.map(item => (
        <Item isMediaActive={store.playingVideo && item.id === store.playingVideo.id} />
      ))}
    </div>
  )
})

It is also better to have that list as "standalone" component, don't just render items inside your whole view. More info here https://mobx.js.org/react-optimizations.html#render-lists-in-dedicated-components

EDIT:

There is also another way, which is actually "more MobX" way of doing things, is to have isPlaying flag in the item object itself. But that might require you to change how you work with your data, so the first example is probably easier if you have already setup everything else.

With flag on the item you don't even need to do anything else, you just check if it is active or not and MobX will do everything else. Only 2 items will rerender when you change the flag. The action in your store could look like that:

playItem(itemToPlay) {
  this.items.find(item => item.isPlaying)?.isPlaying = false

  itemToPlay.isPlaying = true
}
Danila
  • 15,606
  • 2
  • 35
  • 67
  • Thank you for your help, I tried that, but the prop passed to `MediaItem` only updates when the `List` re-renders. Based on the first picture I posted, what I would like to achieve is, when clicking on the last item, to update only the second and last items. – Luc Nov 05 '21 at 10:59
  • But that is exactly what would happen in my example, only 2 items will be updated. And yes, `List` will obviously rerender too. You can't have both, you either rerender the list, or all the items. Rerendering list is cheaper of course. In my example only 3 components will rerender (List and 2 items), in your example all the items will rerender, for example if you have 100 of them then all 100 will rerender which is much more than 3 – Danila Nov 05 '21 at 11:37
  • Again, thank you for your help, I don't want to add the attribute to the video model, but your suggestions allowed me to remove some ill logic and my code is now cleaner. I'm new to this and front-end in general, so some of these things are not obvious to me, sorry about that. I've updated my post with a pseudocode of what I do, could you check it and tell me if anything is wrong ? Currently, nothing re-renders after updating `store.player.videoId` (it's an observable and an action is used for the update) – Luc Nov 05 '21 at 17:25
  • Well, hard to tell now. Your code looks fine, it should work if everything else is correct. Maybe you have something wrong inside your store, if you can post its code too. Would be ideal if you could reproduce it on https://codesandbox.io/ – Danila Nov 05 '21 at 19:42