Skip to content

Latest commit

 

History

History
220 lines (169 loc) · 7.53 KB

error-handling.md

File metadata and controls

220 lines (169 loc) · 7.53 KB

Api Error handling

Handle errors in getServerSideProps with next-connect

ErrorBoundary and Suspense - frontend - loading, data, empty, error states

  • Hashnode tutorial

  • Tkdodo, React Query Error Handling, ErrorBoundary tutorial

  • React Query reset ErrorBoundary, enable Suspense docs

  • ErrorBoundary, Suspense Render-as-you-fetch example (queryClient.prefetchQuery) Codesandbox

  • SWR Suspense example

  • enable Suspense and ErrorBoundary in React Query -suspense: true, useErrorBoundary: true, thats it

  • disable globally ErrorBoundary for a mutation useErrorBoundary: false for isError, error for local Alert

// lib-client/react-query/queryClientConfig.ts

const queryClientConfig: QueryClientConfig = {
  defaultOptions: {
    queries: {
      suspense: true,
      useErrorBoundary: true,
    },
    mutations: {
      useErrorBoundary: false,
    },
  },
  queryCache: new QueryCache({
    onError: (error) => console.error('global Query error handler:', error),
  }),
  mutationCache: new MutationCache({
    onError: (error) => console.error('global Mutation error handler:', error),
  }),
};


// _app.tsx
const { reset } = useQueryErrorResetBoundary();
const [queryClient] = useState(() => new QueryClient(queryClientConfig));

const fallbackRender = (fallbackProps: FallbackProps) => (
  <ErrorFallback {...fallbackProps} fallbackType="screen" />
);

return (
<QueryErrorResetBoundary>
  <ErrorBoundary fallbackRender={fallbackRender} onReset={reset}>
    <Suspense fallback={<Loading loaderType="screen" />}>
    ...
    </Suspense>
  </ErrorBoundary>
</QueryErrorResetBoundary>
);

// test-utils.tsx
// add to wrapper too...
const createTestQueryClient = () =>
  new QueryClient({
    ...queryClientConfig,
    defaultOptions: {
      ...queryClientConfig.defaultOptions,
      queries: {
        ...queryClientConfig.defaultOptions.queries,
        retry: false,
      },
    },
    ...
});
  • useMe overrides default error handler from defaultOptions, React Query default, useHook and mutation options granularity
onError: (error) => {
    console.error('me query error: ', error.response);

    // id exists but not valid session, clear it
    if (id && error.response.status === 404) {
        signOut();
    }
},
  • for safeParse().error type "strict": true is required in tsconfig.json (says in docs)
const result = postsGetSchema.safeParse(req.query);
if (!result.success) throw ApiError.fromZodError(result.error);

// tsconfig.json
"compilerOptions": {
  "strict": true, // true required for zod

Suspense and MeProvider

  • solution: pass await queryClient.prefetchQuery([QueryKeys.ME, me.id], () => me); in every page
  • must be in every page separately or entire app will be server side rendered (no static site generation) - Custom App component Next.js docs
const MeProvider: FC<ProviderProps> = ({ children }) => {
  // prevent inconsistent state Server:x , Client:y error...

  /* Uncaught Error: This Suspense boundary received an update before it finished hydrating. 
  This caused the boundary to switch to client rendering. The usual way to fix this is 
  to wrap the original update in startTransition. */

  const isMounted = useIsMounted();
  const { data } = useMe();

  return (
    <MeContext.Provider value={{ me: data }}>
      {children}
      {/* this causes navbar flashing */}
      {/* {isMounted ? children : null} */}
    </MeContext.Provider>
  );
};
  • if (!data) return null; in views is because of Typescript "strictNullChecks": true, because React Query data has type SomeData | undefined
Error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.
  • Solution: as error says disabled Prev button in pagination has different values on client and server, null and true (page === 1 can return null?), page is state that causes problem when app is still lading/mounting, read error carefully
<Pagination
  isPreviousDisabled={!!page && page === 1} // this
  • React Github issue answer
  • memoize children, seems to work
  • possible whenever you change state in useEffect and !isMounted
const MeProvider: FC<ProviderProps> = ({ children }) => {
  const { data } = useMe();
  const memoChildren = useMemo(() => children, [data]);

  return (
    <MeContext.Provider value={{ me: data ?? null }}>{memoChildren}</MeContext.Provider>
  );
};
// additional fix, useMe
// prevent hook to trigger rerender
enabled: isMounted && status !== 'loading',

Validation Api

  • important: only req.query are strings ([key: string]: string | string[];), req.body preserves correct types (number, boolean), for validation schemas and services argument types

  • you can validate id's too with middleware because of req.query.id

const validateUserCuid = withValidation({
  schema: userIdCuidSchema,
  type: 'Zod',
  mode: 'query',
});

Typescript strictNullChecks

  • non-nullable props stackoverflow

  • env variables types environment.d.ts stackoverflow

  • env var in Node.js is string | undefined, can't be number, must use parseInt(envVar)

  • must be native MouseEvent and not React.MouseEvent or error in addEventListener()

// types

const onClick = (event: MouseEvent) => {
const isInsideAnchor =
  anchorRef.current !== null && anchorRef.current.contains(event.target as Node);

window.addEventListener('click', onClick);
  • make type non-nullable NonNullable<SomeType>

ts-node and tsconfig.json

# custom cwd
ts-node --cwd <path/to/directory>
// yarn add -D tsconfig-paths
// tsconfig.json:
{
  "ts-node": {
    "require": ["tsconfig-paths/register"]
  }
}
  • doesn't work with node in production, don't use it, use relative path in server