solid-js

Why can't I have early returns in Solidjs?


I've just started with Solidjs (coming from React) and I have encountered some strange behaviour. When I have multiple returns, global state is not working. In the case below:

const Test = () => { 
  const [user, setUser] = useUserContext()

  if (!user()) return null
  return ( 
    <div> 
      <div>
      {user()? user()?.username : "No user"}  
      </div>
    </div>
  )
}

if the user is set from another component, this component still returns null (the user is null initially). But if I move that if logic inside the last return (i.e. have only one return), then it works. Why is this happening?

Could not find any resources where this behaviour was mentioned.


Solution

  • SolidJS components are executed once when they are loaded. Unlike React, there is no component-level re-rendering in SolidJS. Because components are not re-executed at a component level, you are limited to the initial value returned by the component.

    JSX elements are re-created or updated when their reactive context is re-executed.

    A reactive context refers to the scope of a computation created by createRenderEffect, createEffect, createMemo, createComputed.

    You have several options.

    Solid runtime creates an implicit reactive context around a reactive value if it is placed inside a JSX element. For example, when the signal updates, the content of the element will be updated by Solid runtime:

    const One = () => {
      return <div>{counter() < 3 ? 'Less Than 3' : 'Equal or greater'}</div>
    }
    

    Alternatively, you can wrap the returned value in a memo:

    const Two = () => {
      return createMemo(() => {
        return counter() < 3 ? <div>Less Than 3</div> : <div>Equal or greater</div>
      }) as unknown as JSXElement;
    }
    

    This approach may seem unconventional, but it reflects how Solid implements components like Show or Suspense. Since memo will re-run when the signal updates, early returns will work as expected.

    Below is a playground to compare these two solutions with early returns and ternaries:

    import { render } from "solid-js/web";
    import { createSignal, createEffect, createMemo, type JSXElement } from "solid-js";
    
    const App = () => {
      const [counter, setCounter] = createSignal<number>(0);
    
      createEffect(() => console.log(counter()));
    
      const One = () => {
        return <div>{counter() < 3 ? 'Less Than 3' : 'Equal or greater'}</div>
      }
    
      const Two = () => {
        return createMemo(() => {
          return counter() < 3 ? <div>Less Than 3</div> : <div>Equal or greater</div>
        }) as unknown as JSXElement;
      }
    
      const Three = () => {
        return counter() < 3 ? <div>Less Than 3</div> : <div>Equal or greater</div>
      }
    
      const Four = () => {
        if (counter() < 3) return <div>Less Than 3</div>;
        return <div>Equal or Greater</div>;
      }
    
      const increment = () => setCounter(prev => prev + 1);
      const decrement = () => setCounter(prev => prev - 1);
      const reset = () => setCounter(0);
    
      return (
        <div>
          <div>Count: {counter()}</div>
          <One />
          <Two />
          <Three />
          <Four />
          <div>
            <button onClick={increment}>Increment</button>
            <button onClick={decrement}>Decrement</button>
            <button onClick={reset}>Reset</button>
          </div>
        </div>
      );
    }
    
    render(() => <App />, document.body);
    

    For posterity, you can use built-in components like Show or Switch/Match for conditional rendering.

    Since user is guaranteed to exist inside the Show component, we can use a non-null assertion for better performance:

    const Test = () => { 
      const [user, setUser] = useUserContext();
      return ( 
        <Show when={user()} fallback={<div>No User</div>}> 
          <div>{user()!.username}</div>
        </Show>
      )
    }