solid-js

Perform actions in child component to reset state after API call in parent


Edited 10/16: For those who stumble on to this, I found this discussion that I thought was really useful: https://github.com/solidjs/solid/discussions/1820. Especially this answer and the reply to it: https://github.com/solidjs/solid/discussions/1820#discussioncomment-8309556. I ended up with the createComponent() : [Component, ...] option, but, I'd like to experiment with the one suggested in the reply.

Original post: I am building a SPA with SolidJS, Vite, and TailwindCSS, and I need architectural help.

I have a page with three components:

  1. Parent App
  2. Create person (contact)
  3. Listing of people (contacts)

The user clicks a button (+), which pops up a small menu-like popup below the button with two fields: full name and email. There are two buttons: Add and clear.

The popup is a separate component, Add New Person. The popup component maintains the state (open, name, email, valid). The component has a property createNewContact, which the parent component passes and is called after the clicks Add, and the form is valid.

The' valid' state is updated as the user interacts with the form (onChange). The component reflects this validation, giving the user real-time feedback on the form's validity.

When the user clicks Add, I'd like to call an API to create the contact. If the creation is successful, the Add New Person component clears the fields and closes. The listing updates to reflect the newly added person.

If there is an error, an error message is presented, and the Add New Person option stays open.

I have taken two approaches:

  1. The Add New Contact component maintains its own state and validity and, via event (passed in createNewContact prop), notifies the parent component for it to take action.

  2. The state is managed in the parent, and it's bound via props to the create component: <CreateNewContact firstname={newContact.firstName} ... addNewContact={handleAddNewContact} />

In both these options, I don't know how to communicate with the child component on what to do next (close & clear, stay open & show error). Any suggestions? One of the things I've seen is an approach like:

let [NewContactForm, openAddNewContact, closeNewContact, clearNewContact] = createNewContactForm();

But, I am not sure if this is the right way to do this in SolidJS.

I prefer approach one because the parent component is responsible for cross-component communication and individual component statement management in approach two. I would like to stay as close as possible to SRP.

Lastly, I am also concerned about testing, but that's a whole different thing. I need to research and refactor my code.

Code sample: https://playground.solidjs.com/anonymous/ecfc2250-580d-4712-94bd-a6b752d47bc9

/*eslint-disable*/

import { render, Portal } from "solid-js/web";
import { For, createSignal, Show } from "solid-js";
import { createStore, produce } from "solid-js/store";


function AddContact(props) {
  let [open, setOpen] = createSignal(false);
  let [newContact, setNewContact] = createStore({ name: "", email: "" });

  function toggleAddNew() {
    setOpen(!open());
  }

  function handleSubmit(e) {
    e.preventDefault();
    let c = {
      name: newContact.name,
      email: newContact.email,
    };
    props.newContact(c);
  }

  function nameChanged(e) {
    setNewContact("name", e.target.value);
  }

  function emailChanged(e) {
    setNewContact("email", e.target.value);
  }

  function clearForm(e) {
    setNewContact("email", "");
    setNewContact("name", "");
  }

  return (
    <>
      <button onClick={toggleAddNew}>Add new contact</button>
      <Show when={open()}>
        <form method="dialog" onsubmit={handleSubmit}>
          <div>
            <span style="margin-right: 10px">Name:</span>
            <input
              type="text"
              placeholder="Please enter a name"
              value={newContact.name}
              onChange={nameChanged}
              name="name"
            />
          </div>
          <div>
            <span style="margin-right: 10px">Email:</span>
            <input
              type="text"
              placeholder="Please enter a e-mail"
              value={newContact.email}
              onChange={emailChanged}
              name="email"
            />
          </div>
          <div>
            <button style="margin-right: 10px;">Add</button>
            <button type="button" onClick={clearForm}>
              Cancel
            </button>
          </div>
        </form>
      </Show>
    </>
  );
}

function ContactList(props) {
  return (
    <ul style="list-style-type: decimal">
      <For each={props.contacts}>
        {(contact, i) => (
          <div>
            <li>
              <span style="margin-right: 10px;">{contact.name}</span>
              <span>{contact.email}</span>
            </li>
          </div>
        )}
      </For>
    </ul>
  );
}


function App() {
  let [contacts, setContacts] = createStore([
    { name: "Jodoc Fito", email: "JodocFito@test.com" },
    { name: "Archie Dragica", email: "ArchieDragica@test.com" },
  ]);

  function newContact(c) {
    /* acts like an API to create the contact */
    setContacts(
      produce((contacts) => {
        contacts.push(c);
      }),
    );

    /* 
    close form
    clear new contact form

    */
  }

  return (
    <>
      <section>
        <AddContact newContact={newContact} />
      </section>
      <section>
        <ContactList contacts={contacts} />
      </section>
    </>
  );
}
render(() => <App />, document.getElementById("app")!);


Solution

  • Your approach seems fine to me, you just need to finish the AddContact component as follows:

    function AddContact(props) {
      let [open, setOpen] = createSignal(false);
    
      // could also simply use two createSignals instead of createStore
      let [newContact, setNewContact] = createStore({ name: "", email: "" });
    
      function onSubmit(e) {
        e.preventDefault();
        props.newContact({
          name: newContact.name,
          email: newContact.email,
        });
        setNewContact({ name:'', email: ''});
        setOpen(false);
      }
    
      function onCancel(e) {
        e.preventDefault();
        setNewContact({ name:'', email: ''});
        setOpen(false);
      }
    
      return (
        <Show
          when={open()}
          fallback={<button onClick={() => setOpen(true)}>Add new contact</button>}
          >
          <form onsubmit={onSubmit}>
            <label>
              <span>Name:</span>
              <input
                type="text"
                value={newContact.name}
                onChange={e => setNewContact("name", e.target.value)}
              />
            </label>
            <label>
              <span>Email:</span>
              <input
                type="text"
                value={newContact.email}
                onChange={e => setNewContact("email", e.target.value)}
              />
            </label>
    
            <div>
              <button>Add</button>
              <button onClick={onCancel}>Cancel</button>
            </div>
          </form>
        </Show>
      );
    }
    

    Note that I've also added proper <label>s, which is important for screen readers and now you can click on the label to focus the text input.

    I would also recommend reading Solid Docs: Complex State Management and React Docs: Choosing State Structure and following chapters.