generator function doesn't resume after yield

345 Views Asked by At

So I'm trying to debug why my array of questions is not being properly populated in my mobx-state-tree questions-store. Nothing after the yield statement in the getQuestions action runs.

// question-store.ts
import { Instance, SnapshotOut, types, flow } from "mobx-state-tree"
import { Question, QuestionModel, QuestionSnapshot } from "../question/question";
import { withEnvironment } from '../';
import { GetQuestionsResult } from "../../services/api";

    export const QuestionStoreModel = types
      .model("QuestionStore")
      .props({
        questions: types.optional(types.array(QuestionModel), [])
      })
      .extend(withEnvironment)
      .views((self) => ({})) // eslint-disable-line @typescript-eslint/no-unused-vars
      .actions((self) => ({
        saveQuestions: (questionSnapshots: QuestionSnapshot[]) => {
          console.tron.log("SAVE QUESTIONS");
          console.tron.log({ questionSnapshots });
          const questionModels: Question[] = questionSnapshots.map(c => QuestionModel.create(c));
          self.questions.replace(questionModels); // Replace the existing data with the new data
        }
      })) 
      .actions((self) => ({
        getQuestions: flow(function*() {
          console.tron.log("GET QUESTIONS");
          const result: GetQuestionsResult = yield self.environment.api.getQuestions(); // Nothing after this yield statement runs
          console.tron.log("AFTER GET QUESTIONS");
    
          if (result.kind === "ok") {
            self.saveQuestions(result.questions);
          } else {
            __DEV__ && console.tron.log(result.kind);
          }
        })
      }))
type QuestionStoreType = Instance<typeof QuestionStoreModel>
export interface QuestionStore extends QuestionStoreType {}
type QuestionStoreSnapshotType = SnapshotOut<typeof QuestionStoreModel>
export interface QuestionStoreSnapshot extends QuestionStoreSnapshotType {}
export const createQuestionStoreDefaultModel = () => types.optional(QuestionStoreModel, {})

This is the api's getQuestions

import { ApisauceInstance, create, ApiResponse } from "apisauce"
import { getGeneralApiProblem } from "./api-problem"
import { ApiConfig, DEFAULT_API_CONFIG } from "./api-config"
import * as Types from "./api.types"
import uuid from 'react-native-uuid';
import { QuestionSnapshot, Question } from "../../models";

const API_PAGE_SIZE = 5;

const convertQuestion = (raw: any): QuestionSnapshot => {
  const id = uuid.v4().toString();

  return {
    id: id,
    category: raw.category,
    type: raw.type,
    difficulty: raw.difficulty,
    question: raw.question,
    correctAnswer: raw.correct_answer,
    incorrectAnswers: raw.incorrect_answers,
  }
}

/**
 * Manages all requests to the API.
 */
export class Api {
  /**
   * The underlying apisauce instance which performs the requests.
   */
  apisauce: ApisauceInstance

  /**
   * Configurable options.
   */
  config: ApiConfig

  /**
   * Creates the api.
   *
   * @param config The configuration to use.
   */
  constructor(config: ApiConfig = DEFAULT_API_CONFIG) {
    this.config = config
  }

  /**
   * Sets up the API.  This will be called during the bootup
   * sequence and will happen before the first React component
   * is mounted.
   *
   * Be as quick as possible in here.
   */
  setup() {
    // construct the apisauce instance
    this.apisauce = create({
      baseURL: this.config.url,
      timeout: this.config.timeout,
      headers: {
        Accept: "application/json",
      },
    })
  }
    async getQuestions(): Promise<Types.GetQuestionsResult> {
        // make the api call
        const response: ApiResponse<any> = await this.apisauce.get("", { amount: API_PAGE_SIZE })
        console.tron.log({response});
        // the typical ways to die when calling an api
        if (!response.ok) {
          const problem = getGeneralApiProblem(response);
          if (problem) return problem;
        }
        console.tron.log('AFTER OK CHECK')
    
        // transform the data into the format we are expecting
        try {
          const rawQuestion = response.data.results;
          console.tron.log({rawQuestion});
          const convertedQuestions: QuestionSnapshot[] = rawQuestion.map(convertQuestion);
          console.tron.log({convertedQuestions});
          return { kind: "ok", questions: convertedQuestions };
        } catch (e) {
          __DEV__ && console.tron.log(e.message);
          return { kind: 'bad-data' }
        }
      }
  async getUsers(): Promise<Types.GetUsersResult> {
    // make the api call
    const response: ApiResponse<any> = await this.apisauce.get(`/users`)

    // the typical ways to die when calling an api
    if (!response.ok) {
      const problem = getGeneralApiProblem(response)
      if (problem) return problem
    }

    const convertUser = (raw) => {
      return {
        id: raw.id,
        name: raw.name,
      }
    }

    // transform the data into the format we are expecting
    try {
      const rawUsers = response.data
      const resultUsers: Types.User[] = rawUsers.map(convertUser)
      return { kind: "ok", users: resultUsers }
    } catch {
      return { kind: "bad-data" }
    }
  }

  /**
   * Gets a single user by ID
   */

  async getUser(id: string): Promise<Types.GetUserResult> {
    // make the api call
    const response: ApiResponse<any> = await this.apisauce.get(`/users/${id}`)

    // the typical ways to die when calling an api
    if (!response.ok) {
      const problem = getGeneralApiProblem(response)
      if (problem) return problem
    }

    // transform the data into the format we are expecting
    try {
      const resultUser: Types.User = {
        id: response.data.id,
        name: response.data.name,
      }
      return { kind: "ok", user: resultUser }
    } catch {
      return { kind: "bad-data" }
    }
  }
}

And the Question Model:

import { Instance, SnapshotOut, types } from "mobx-state-tree"
import { shuffle } from "lodash";
    export const QuestionModel = types
      .model("Question")
      .props({
        id: types.identifier,
        category: types.maybe(types.string), 
        type: types.enumeration(['multiple', 'boolean']),
        difficulty: types.enumeration(['easy', 'medium', 'hard']),
        question: types.maybe(types.string), 
        correctAnswer: types.maybe(types.string), 
        incorrectAnswers: types.optional(types.array(types.string), []),
        guess: types.maybe(types.string)
      })
      .views((self) => ({
        get allAnswers() {
          return shuffle(self.incorrectAnswers.concat([self.correctAnswer]))
        },
        get isCorrect() {
          return self.guess === self.correctAnswer;
        }
      })) 
      .actions((self) => ({
        setGuess(guess: string) {
          self.guess = guess;
        }
      })) 
    
    type QuestionType = Instance<typeof QuestionModel>
    export interface Question extends QuestionType {}
    type QuestionSnapshotType = SnapshotOut<typeof QuestionModel>
    export interface QuestionSnapshot extends QuestionSnapshotType {}
    export const createQuestionDefaultModel = () => types.optional(QuestionModel, {})

Debug logs

As you can see from the logs, the api call is successful and convertedQuestions logs properly but nothing is recorded after that. Maybe I'm misunderstanding how generator functions work but shouldn't it resume after the yielding function returns a value?

Any insights would be greatly appreciated.

1

There are 1 best solutions below

1
On

tl;dr: In QuestionStoreModel check that your import of flow comes from mobx-state-tree, not mobx:

import { flow } from "mobx-state-tree";

Your code looks perfect, so I'm guessing the issue surrounds the use of flow().

From the mst docs on Asynchronous Actions:

Warning: don't import flow from "mobx", but from "mobx-state-tree" instead!

They don't provide details on what would happen if the imports are incorrect, but I'd assume it would affect yield statements.