0

I am learning ReactJs and I ran into some question when trying out the useState and useContext hook. It seems the re-rendering will only happen if there is reference change rather than value change. Take the bellowing code for example:

https://stackblitz.com/edit/react-ts-4gw3h1

class Counter {
   count: number;
   constructor(count: number) {
     this.count = count;
   }
   
   increase() {
     this.count += 1;
   }
}

export const CountComponent : FC = (props) => {
   //const [count, setCount] = useState(0); // work
   //const [count, setCount] = useState({0}); // work
   const [counter] = useState(new Counter(0)); // not work

   const doAdd = () => {
      counter.increase();
      console.log(counter.count);
   }
    
   // this will not actually re-render 
   return (<div>{counter.count}<button onClick={doAdd}>+</button></div>);
}

As you click the button, the count has increased yet the value rendered isn't updated. Am I missing anything there? And is there a way to have the component monitor the state object property change?

EDIT

I think I have confused people with intention of the question. Consider a more complicated use case where I have a complex state object. Pardon me with the psuedocode:

class Cargo {
    orders: Map<string, Products[]>;
    totalValue: number;
    totalNumber: number;

    addProduct(product: Product) {
      // ignore the null check here.
      this.orders.get(product.owner).push(product);
      this.totalValue += product.value;
      this.totalNumber ++;
    }
}

class Product {
   owner: string;
   name: string;
   value: number;
}

const CargoContext = createContext(new Cargo());

const CargoContext = createContext({... new Cargo(), setTotalValue, setTotalNumber});

// lost the OO
const {orders, totalValue, totalNumber, setTotalValue, setTotalNumber} = useContext(CargoContext);

const doAdd = (product: Product) => {
   orders.get(product.owner); 
   setTotalValue(product.value);
   setTotalNumber(totalNumber + 1);
}

When creating a context this way, I won't be able to update the state actually. The only way to make it work from what I read is to deconstruct the Cargo object and passing the property and dispatcher. But that lose the object-oriented Cargo and addProduct interface. How to deal with this case more gracefully?

8
  • 1
    You're not using state correctly. How does React know state has changed if you don't tell it that state has changed? You need to use the setCounter function. The update function does more than just change the state value. It asks React to compare the old and new values and schedule a render if they're different. The comparison is also a shallow compare, so since your state is an object, the ref needs to change, not just a value inside it. Commented Jun 26, 2020 at 20:24
  • useState returns 2 things - its current value and the updater function. You should destructure with const [counter, setCounter] ... and use setCounter to update that state, which will trigger a re-render Commented Jun 26, 2020 at 20:24
  • The good news is its actually easier than you've made it! The Counter class is unnecessary. Just give useState the initial value of 0 and you're good to go! (while also following the suggested commented above) Commented Jun 26, 2020 at 20:26
  • Thanks for the input. I realized I am not presenting my intention precisely. I have edited my question. Commented Jun 26, 2020 at 20:33
  • 2
    I believe your issue is that you are writing functional code but trying to use an OO paradigm. Just stick with the functional approach. Also, your context needs a provider, which you currently don't have. Your provider should store the cargo properties in state and expose them through the context API Commented Jun 26, 2020 at 20:45

1 Answer 1

1

When dealing with complex state like that it is best to use useReducer. To go along with your cargos example it would like something like

interface CargoState {
    orders: Map<string, Products[]>;
    totalValue: number;
    totalNumber: number;
}

const cargoReducer = (state: CargoState, action): CargoState => {
   switch (action.type) {
       case 'add-product': {
           const newOrders = state.orders;
           newOrders.get(action.product.name).push(action.product);

           return {
               ...state,
               orders: newOrders,
               totalValue: state.totalValue + product.value,
               totalNumber: state.totalNumber++,
           }
       }
       default: return state;
    }
}



const [state, dispatch] = useReducer(cargoReducer, [some initial state here]);


dispatch({ type: 'add-product', product });

The actions can be typed properly so you are not losing any typing.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you @topchd. This is a great solution!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.