Home > Back-end >  Why React component doesn't re-render (change styles) when props changed?
Why React component doesn't re-render (change styles) when props changed?

Time:09-04

I made a TagItem JSXElement that color changes when a user clicks/touches.

It receives a prop isSelected and it also has its own state selected inside, which initial value is prop isSelected. Its style changes depending on selected state but the style doesn't change when prop isSelected value changes...

The reason I figured out is that its state doesn't change even when prop isSelected changes.

I don't know why this happens. Does anyone know?

The code is like this.

type TagItemProps = {
      tag: Tag;
      isSelected: boolean;
      onSelect: (tag: Tag) => boolean;
      onDeselect: (tag: Tag) => void;
    };
    
const TagItem = ({ tag, isSelected = false, onSelect, onDeselect }: TagItemProps) => {
      const [selected, setSelected] = useState(isSelected);

      console.log("isSelected: ", isSelected);
      console.log("selected: ", selected);
    
      const handleSelect = useCallback(() => {
        if (!onSelect(tag)) return;
        setSelected(true);
      }, [onSelect, tag]);
    
      const handleDeselect = useCallback(() => {
        onDeselect(tag);
        setSelected(false);
      }, [onDeselect, tag]);
    
      return (
        <TouchableOpacity
          style={[styles.container, selected ? styles.selected : null]}
          onPress={selected ? handleDeselect : handleSelect}
        >
          <Text style={[styles.text, selected ? { color: '#fff' } : null]}>
            {capitalizeFirstLetter(tag.name)}
          </Text>
        </TouchableOpacity>
      );
};
    
export default TagItem;
    
const styles = StyleSheet.create({
      container: {
        flex: 0,
        backgroundColor: '#fff',
        borderRadius: 20,
        paddingHorizontal: 10,
        paddingVertical: 12,
        margin: 5,
      },
      selected: {
        backgroundColor: 'green',
      },
      text: {
        textAlign: 'center',
        fontWeight: 'bold',
      },
});

As you can see from above, when state selected is true, styles.selected should be applied.

I confirm that prop isSelected changes but not selected state, by logging in console.

console.log("isSelected: ", isSelected); // false
console.log("selected: ", selected);

Does anyone know why this is happening?

CodePudding user response:

This is a classic case of copying props into local state -- which you usually do not want to do as it introduces a surface area for bugs, including the one you are seeing. If something is available as a prop -- what is the purpose of copying it into local state? You should instead use callbacks to alter wherever the state of that prop lives in the ancestors. Copying means you now have to manage keeping the local state and prop in sync -- which is why usually copying in the first place is an antipatttern.

The reason the state doesn't update when isSelected changes is because the paramater to useState is only its initial value. By design, even when a rerender occurs due to the prop changing, the state item wont update. Copying it into local state means its up to you to keep them in sync (common cause of bugs).

Two choices:

Option A

Dont copy props into state, so you dont even need to deal with making sure the props and internal state are in sync. Use isSelected directly and remove the state item. To set the selected state, you will need to pass down into the component from the parent a callback over props -- which accepts the changed value and changes the state in that parent component. This gets rid of the pointless barrier inbetween the props and the actual thing you are rendering.

Option B

If you must keep a copy of the state around for some reason, make sure you update the state when the props change with an additional effect.

type TagItemProps = {
      tag: Tag;
      isSelected: boolean;
      onSelect: (tag: Tag) => boolean;
      onDeselect: (tag: Tag) => void;
    };
    
const TagItem = ({ tag, isSelected = false, onSelect, onDeselect }: TagItemProps) => {
      const [selected, setSelected] = useState(isSelected);

      useEffect(() => {
          setSelected(isSelected )
     }, [isSelected ])

      console.log("isSelected: ", isSelected);
      console.log("selected: ", selected);
    
      const handleSelect = useCallback(() => {
        if (!onSelect(tag)) return;
        setSelected(true);
      }, [onSelect, tag]);
    
      const handleDeselect = useCallback(() => {
        onDeselect(tag);
        setSelected(false);
      }, [onDeselect, tag]);
    
      return (
        <TouchableOpacity
          style={[styles.container, selected ? styles.selected : null]}
          onPress={selected ? handleDeselect : handleSelect}
        >
          <Text style={[styles.text, selected ? { color: '#fff' } : null]}>
            {capitalizeFirstLetter(tag.name)}
          </Text>
        </TouchableOpacity>
      );
};
    
export default TagItem;
    
const styles = StyleSheet.create({
      container: {
        flex: 0,
        backgroundColor: '#fff',
        borderRadius: 20,
        paddingHorizontal: 10,
        paddingVertical: 12,
        margin: 5,
      },
      selected: {
        backgroundColor: 'green',
      },
      text: {
        textAlign: 'center',
        fontWeight: 'bold',
      },
});

CodePudding user response:

Just leaving the code I ended up with (@Adam 's option A) and it works totally fine.

type TagItemProps = {
  tag: Tag;
  isSelected: boolean;
  onSelect?: (tag: Tag) => boolean;
  onDeselect?: (tag: Tag) => void;
};

const TagItem = ({ tag, isSelected, onSelect, onDeselect }: TagItemProps) => {
  const handleSelect = useCallback(() => {
    onSelect && onSelect(tag);
  }, [onSelect, tag]);

  const handleDeselect = useCallback(() => {
    onDeselect && onDeselect(tag);
  }, [onDeselect, tag]);

  return (
    <TouchableOpacity
      style={[styles.container, isSelected ? styles.selected : null]}
      onPress={isSelected ? handleDeselect : handleSelect}
    >
      <Text style={[styles.text, isSelected ? { color: '#fff' } : null]}>
        {capitalizeFirstLetter(tag.name)}
      </Text>
    </TouchableOpacity>
  );
};
  • Related