Search code examples
solid-js

SolidJS Context not passing down to children


I am creating a registration form, and along with that, I made a form context provider for handling validation, and an InputText component to handle display.

The context from FormContextProvider however is not being passed down to the form InputText components.

Here are the three components:

Register.tsx

import Joi from "joi";
import FormContextProvider from "../../components/forms/FormContextProvider/FormContextProvider";
import InputText from "../../components/forms/InputText/InputText";

const registrationSchema = Joi.object({
  email: Joi.string().email({ tlds: false }).required(),
  username: Joi.string().alphanum().min(3).max(30).required(),
  password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")),
  repeat_password: Joi.ref("password"),
}).with("password", "repeat_password");

function registrationSchemaMessageRewrite(validationResult: Joi.ValidationResult) {
  if (validationResult.error?.details?.length) {
    validationResult.error.details.forEach((error) => {
      if (error.context?.key === "repeat_password") {
        error.message = "Passwords do not match";
      }
    });
  }
  return validationResult;
}

export default function Register() {
  const initialData = { username: "", password: "", repeat_password: "" };
  return (
    <FormContextProvider initialData={initialData} validation={registrationSchema} postValidation={registrationSchemaMessageRewrite}>
      <InputText label="Username" name="username" type="text" />
      <InputText label="Email" name="email" type="email" />
      <InputText label="Password" name="password" type="password" />
      <InputText label="Repeat Password" name="repeat_password" type="password" />
      <div>
        <input type="checkbox" name="terms" /> I agree to the terms and conditions
      </div>
      <input type="submit" value="Submit" />
    </FormContextProvider>
  );
}

FormContextProvider.tsx

import type Joi from "joi";
import { Accessor, createContext, createSignal } from "solid-js";

export const FormContext = createContext<Form>({
  data: {},
  set: () => undefined,
  validationResult: () => undefined,
  touched: () => ({}),
  setFieldValue: () => undefined,
  inputChangeHandler: () => () => undefined,
});

export interface Form {
  data: any;
  set: (data: any) => void;
  validationResult: Accessor<Joi.ValidationResult<any> | undefined>;
  touched: Accessor<TouchedData>;
  setFieldValue: (field: string, value: string) => void;
  inputChangeHandler: (name: string) => (e: Event) => void;
}

export interface FormContextProviderProps {
  children: any;
  initialData: { [key: string]: any };
  validation: Joi.ObjectSchema<any>;
  postValidation: (validationResult: Joi.ValidationResult<any>) => Joi.ValidationResult<any>;
}

interface FormData {
  [key: string]: string;
}
interface TouchedData {
  [key: string]: boolean;
}

function defaultPostValidation(validationResult: Joi.ValidationResult<any>) {
  return validationResult;
}

declare module "solid-js" {
  namespace JSX {
    interface Directives {
      formDecorator: boolean;
    }
  }
}

// wrap children in provider
export default function FormContextProvider(props: FormContextProviderProps) {
  const { children, initialData, validation, postValidation = defaultPostValidation } = props;
  const [formData, setFormData] = createSignal<FormData>(initialData || {});
  const initialTouchData: TouchedData = { test: true };
  const [touched, setTouched] = createSignal<TouchedData>(initialTouchData);
  const [validationResult, setValidationResult] = createSignal<Joi.ValidationResult<any> | undefined>();

  function validate(): boolean {
    setValidationResult(postValidation(validation.validate(formData(), { abortEarly: false })));
    if (validationResult()?.error) {
      return false;
    } else {
      return true;
    }
  }

  function formDecorator(element: HTMLFormElement, _: () => any): void {
    element.addEventListener("submit", async (e) => {
      e.preventDefault();
      if (validate()) {
        console.log("validation passes");
      } else {
        console.log("validation fails");
      }
    });
  }
  true && formDecorator; // hack to prevent unused variable error

  const form = {
    data: formData,
    set: setFormData,
    validationResult,
    touched,
    setFieldValue: (field: string, value: string) => {
      setFormData({ [field]: value });
    },
    inputChangeHandler: (name: string) => (e: Event) => {
      const target = e.target as HTMLInputElement;
      setFormData({ ...formData(), [name]: target.value });
      setTouched({ ...touched(), [name]: true });
      validate();
    },
  };
  validate();
  return (
    <FormContext.Provider value={form}>
      <form use:formDecorator>{children}</form>
    </FormContext.Provider>
  );
}

InputText.tsx

import { createEffect, createSignal, useContext } from "solid-js";
import { FormContext } from "../FormContextProvider/FormContextProvider";

interface InputTextProps {
  label: string;
  name: string;
  type: string;
}

export default function InputText(props: InputTextProps) {
  const { label, name, type } = props;
  const form = useContext(FormContext);
  console.log(form);
  if (form === undefined) {
    return <div>FormContextProvider not found</div>;
  }

  const [error, setError] = createSignal("");

  createEffect(() => {
    let updatedError = "";
    const errorDetails = form.validationResult()?.error?.details;
    if (!errorDetails || !errorDetails.length) return setError(updatedError);
    errorDetails.forEach((error) => {
      if (error.context?.key === name) {
        updatedError = error.message;
      }
    });
    return setError(updatedError);
  });

  return (
    <div>
      <input type={type} name={name} id={name} placeholder={label} value={form.data[name] || ""} onInput={form.inputChangeHandler(name)} />
      <div class="error">{form.touched()?.[name] && error()}&nbsp;</div>
    </div>
  );
}

The console.log(form) just returns the default value from createContext<Form>(/*defaults*/) in FormContextProvider, but I want it to return the values set in <FormContext.Provider value={form}>


Solution

  • This is caused by the destructuring of props in FormContextProvider:

      const { children, initialData, validation, postValidation = defaultPostValidation } = props;
    

    By removing this line and referring to individual props by prefixing with "props." like "props.children" this problem was resolved. This is because by destructuring props you lose the reactivity (updates to the variable won't update the page content).

    Most likely only children needed to remain on props as the other ones are static.

    If so, something like this could be done:

     const [{ initialData, validation, postValidation }, local] = splitProps(props, ["initialData", "validation", "postValidation"]);
    

    and refer to children by local.children