Is it safe to share class instance in React Context API?

Viewed 1698

I am building a quiz game in React Native and am using Redux-Thunk to store a game object, which is created at the beginning of the game.

It stores things like:

  • gamemode, players, questions, current round, current player, current question (as indexes)

The problem with redux-thunk is handling different gamemodes and logic.

Is it a good practice to use a game class like that:

class Game {
  constructor() {
    Object.assign(this, DEFAULT_STATE);
  }

  init = (gamemode, players) => {
    // set Gamemode
    switch (gamemode) {
      case "classic":
        this.gamemode = new Classic();
        break;
    }
    // set Players
    this.players = players;

    // set Gameparameters
    this.parameters = this.gamemode.getGameParameters();

    // set Questions
    this.questions = this.gamemode.prepareQuestions();
  };

  // Getters
  getPlayerCount = () => this.players.length;
  getCurrentRound = () => this.currentRound + 1;
  getCurrentRoundIndex = () => this.currentRound;
}

and share it in a Provider through Context API to access its state and functions?

1 Answers

This is a very broad question. I'll answer it with my own opinion about how I would likely handle a situation like this.

Since React 16.3, I've found the built-in context api to be easier to work with, and more powerful, than Redux. This is how I would setup the context provider:

const DEFAULT_STATE = {
  gameMode: 'default',
  players: [],
  currentRound: 1,
  ...
};

export const MyContext = React.createContext(DEFAULT_STATE);
export const MyConsumer = MyContext.Consumer;

class MyProvider extends Component {
  state = DEFAULT_STATE;

  componentDidMount() {
    // query your api on app start here
  }

  render() {
    return (
     <MyContext.Provider
       value={{
         state: this.state,
         setState: (name, value, callback = () => {}) => {
           this.setState({ [name]: value }, callback);
         },
         incrementRound: () => {
           const { currentRound } = this.state;
           this.setState({ currentRound: currentRound + 1 });
         },
         // any other functions you want to expose to your consumer
       }}
     >
      {this.props.children}
     </MyContext.Provider>
  }
};

export default MyProvider;

You could then setup your root component like this:

ReactDOM.render(
 <MyProvider><App /></MyProvider>,
 document.getElementById('root')
);

Then you can access your context/consumer in any of your other components:

class SomeComponent extends Component {
  state = {/* component state */};

  incrementRound = (context) => {
    // option a: access state directly
    const { currentRound } = context.state;
    context.setState('currentRound', currentRound + 1);
    // option b: expose function to handle state
    context.incrementState();
  };      

  render() {
    return (
      <section>
        <div>{/* some other stuff */}</div>
        <MyConsumer>
          {context => {
            const { currentRound } = context.state;
            return (
             <div>{currentRound}</div>;
             <button>Click to update round!</button>
            );
          }}
        </MyConsumer>
      </section>
    );
  }
}

This is a very simple example, but I think it gets the point across. If you go this route, I would also recommend exporting your state keys from your DEFAULT_STATE object so that you don't have call setState with literal strings. You could do something like this:

const stateKeysObj = {};
Object.keys(DEFAULT_STATE).forEach(key => {
  stateKeysObj[key] = key;
});
export const stateKeys = Object.freeze(stateKeysObj);

Good luck with your game!

Related