Today I Learned

4 posts by tomaszromik @Skysplot

Use GraphQL schema to create TS types and react hooks

Very often when working with backend it is useful for frontend application to know what response can we get.

When working with TypeScript we can actually generate types and hooks using schema.graphql or even “live” backend.

This is where graphql-codegen comes in handy

When you need to create generic components for resources (types) shared across different components you can use typescript plugin

When writing graphql queries, you can also generate type-safe react hooks with typescript-react-apollo plugin. With this plugin you only need to write *.graphql with desired Query, Mutation, Subscription or Fragment (you don’t need to import fragments in your quries!) and respective hooks will be created.

Example configuration codegen.yaml file from project:

overwrite: true

schema: 'http://localhost:4000/graphql'

documents:
  - 'src/**/queries.ts'
  - 'src/**/*.graphql'

generates:
  src/__generated__/types.tsx:
    plugins:
      - 'typescript'
    config:
      maybeValue: T | undefined

  src/__generated__/operations.tsx:
    preset: import-types
    presetConfig:
      typesPath: ./types
    plugins:
      - typescript-operations
    config:
      maybeValue: T | undefined

  src/__generated__/hooks.tsx:
    preset: import-types
    presetConfig:
      typesPath: ./operations
    plugins:
      - typescript-react-apollo
    config:
      maybeValue: T | undefined

Generic with class as argument and returning instance

So let’s imagine you have few classes. Let’s say it’s just something like

class Foo {};
class Bar {};
class Baz {};

You want to create a function that accepts a class as an argument and based on that, it returns class instance.

The best approach would be to have TS generics.

function create<T>(model: T): T {
    // Error!
    // This expression is not constructable.
    // Type '{}' has no construct signatures.
    return new model();
}

If we use it like that, we’ve got an error that type doesn’t have construct signatures.

Why is that?

It’s because, when you are defining some variable or agument to be type of a class, what you actually mean is that this variable needs to be instance of given class!

So how can we tell typescript that we want class constructor?

We can tell typescript that argument (or variable) needs to be class constructor using new () => Type or { new(): Type }

function create<T>(model: new () => T): T {
    return new model();
}


create(Foo); // returns Foo instance
create(Bar); // returns Bar instance
create(Baz); // returns Baz instance

However, there is one catch here! new () needs to have the same signature as class constructor, otherwise it will throw an error as well

class Foo {
    constructor(a: number) {
    }
}

create(Foo); // Error!

You can also work around it by having

new (...args: any[]) => T

…that is if you don’t care about constructor arguments much… ;)

Use formik with context/hooks instead of render-props

Note that this feature is available in formik >= 2.0

Usually when you’re creating forms using formik you access formik props using render-prop pattern

export function MyForm() {
  return (
    <Formik
      initialValues={{ /* something here */ }}
      onSubmit={(values, actions) => {/* handler here */}}
    >
      {(formikProps) => (
        <form onSubmit={formikProps.handleSubmit}>
          <MyInputComponent value={formikProps.values.name} />
        </form>
      )}
    </Formik>
  )
}

Instead you can use formik as context provider (which it is under the hood actually) and use useFormikContext() hook or FormikConsumer component to consume provided form state.

Example below:

export function MyFormContainer() {
  return (
    <Formik
      {/* Under the hood Formik uses Context Provider */}
      initialValues={{ /* something here */}}
      onSubmit={(values, actions) => { /* handler here */ }}
    >
      {/* You do not have to use function here! */}
      {/* However you can, if you would like to :) */}
      <MyForm />
    </Formik>
  )
}

export function MyForm() {
  const { handleSubmit, values } = useFormikContext(); // formikProps

  return (
    <form onSubmit={handleSubmit}>
      <MyInputComponent value={values.name} />
    </form>
  );
}

This allows us to use formik props deep down in our nested components without drilling down formikProps.