reactjsmobxreact-functional-componentmobx-reactcomputed-observable

Mobx only re-render item when computed value changes


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.


Solution

  • 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
    }