diff --git a/flagsmith-core.ts b/flagsmith-core.ts index d81d05e..3e56587 100644 --- a/flagsmith-core.ts +++ b/flagsmith-core.ts @@ -623,6 +623,13 @@ const Flagsmith = class { this.evaluationEvent = state.evaluationEvent || this.evaluationEvent; this.identity = this.getContext()?.identity?.identifier this.log("setState called", this) + // Hydration from server state (e.g. + // without an options prop) must flip loadingState to loaded. Guarded on + // source===NONE so we never clobber a loaded state already set by + // init()/getFlags()/cache paths that also call setState internally. + if (this.loadingState.source === FlagSource.NONE && this.flags && Object.keys(this.flags).length > 0) { + this.setLoadingState(this._loadedState(null, FlagSource.SERVER)); + } } } diff --git a/test/react.test.tsx b/test/react.test.tsx index cc996e0..f67d1e6 100644 --- a/test/react.test.tsx +++ b/test/react.test.tsx @@ -186,6 +186,25 @@ describe('FlagsmithProvider', () => { expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(defaultState.flags)) }) }) + it('reports a loaded state when hydrated from serverState with no options', async () => { + const { flagsmith } = getFlagsmith({}) + render( + + + , + ) + + // Flags are available from serverState immediately. + expect(JSON.parse(screen.getByTestId('flags').innerHTML)).toEqual(removeIds(defaultState.flags)) + + // Loading state must not be stuck on isLoading/isFetching=true. + await waitFor(() => { + const loadingState = JSON.parse(screen.getByTestId('loading-state').innerHTML) + expect(loadingState.isLoading).toBe(false) + expect(loadingState.isFetching).toBe(false) + expect(loadingState.error).toBeNull() + }) + }) it('ignores init response if identify gets called and resolves first', async () => { const onChange = jest.fn() const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange })