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, {})
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.
tl;dr: In QuestionStoreModel check that your import of
flow
comes frommobx-state-tree
, notmobx
:Your code looks perfect, so I'm guessing the issue surrounds the use of
flow()
.From the mst docs on Asynchronous Actions:
They don't provide details on what would happen if the imports are incorrect, but I'd assume it would affect yield statements.