The smell of optional chaining on hooks

Optional chaining is cool. Is very cool. But at the same time, it’s the biggest code smell we inherited from the unstructured mess of the dynamic language named Javascript. It shows that the code does not trust the object it handles, so it needs to create some sort of safety net. It shows that the developer compensates for some other code smell.

Optional chaining to handle later-to-come props

I commonly see on react components conditional statements, meant to handle elements that will be filled later on.

const SomeComponent = ({textObject}) => {
    if(!textObject) return null;

    const [someObject, setSomeObject] = React.useState({...textObject,metaData: {length: textObject.text.length}});

    useInitializedEffect(()=>{
        setSomeObject({...textObject,metaData: {length: textObject.text.length}});
    }, [textObject]);

    return <div>
        {someObject.text} ({someObject.metaData.length})
    </div>;
}

OK, I don’t see this exactly, since hooks need to be consistent and with consistent order. So the developer needs to move the condition, and add plenty of optional chaining.

const SomeComponent = ({textObject}) => {
    const setValue = (textObject) => {
        return {...textObject,metaData: {length: textObject?.text.length}};
    }

    const [someObject, setSomeObject] = React.useState(setValue(textObject));

    useInitializedEffect(()=>{
        setSomeObject(setValue(textObject));
    }, [textObject]);

    if(!textObject) return null;

    return <div>
        {someObject.text} ({someObject.metaData.length})
    </div>;
}

Now think how much code each developer will need to add, just to secure his component. Hiding the return null somewhere deep after we executed plenty of unnecessary code. Another problem is that this type of code can render plenty of unneeded elements, that each one secures himself with its own safety net. The larger bigger problem is that the parent competent was not supposed to render it in the first place.

Most of the time the reason we pass nullable prop is since the parent component did not receive it yet.

const App = () => {
    const [textObject, setTextObject] = React.useState();

    React.useEffect(()=>{
        setTimeout(()=>setTextObject({text:'stage 1.3'}),500);
    },[])

    return <SomeComponent textObject={textObject} />
}

We can use the official common ugly hack:

const App = () => {
    const [textObject, setTextObject] = React.useState();

    React.useEffect(()=>{
        setTimeout(()=>setTextObject({text:'stage 1.3'}),500);
    },[])

    return <> { textObject && <SomeComponent textObject={textObject} />} </>
}

This pattern will not help us if the component depends on some inner values. A simple example will be:

const SomeComponent = ({textObject}) => {
    if(!textObject.text) return null;
    
    const setValue = (textObject) => {
        return {...textObject,metaData: {length: textObject?.text.length}};
    }

    const [someObject, setSomeObject] = React.useState(setValue(textObject));

    useEffect(()=>{
        setSomeObject(setValue(textObject));
    }, [textObject]);

    return <div>
        {someObject.text} ({someObject.metaData.length})
    </div>;
}

But one more, we cant add the condition before the hooks, and if we will want to move it after the hook, we are back to smelly code that needs to protect itself from undefined values.

The other side of the same optional chaining smell

But let’s say we have a component that needs some initializing to run.

const SomeComponent = ({textObject}) => {
    const  initExpensiveMethod = (textObject)=>{
        return {test:textObject?.text,length : textObject?.text?.length};
    };

    const [someObject, setSomeObject] = React.useState(initExpensiveMethod(textObject));

    React.useEffect(()=>{
        setSomeObject(initExpensiveMethod(textObject));
    }, [textObject]);

    return <div>
        {someObject.text} ({someObject.length})
    </div>;
}

The problem is that the useEffect executes the init method on the first run, so we are executing the method twice. What if we have some expensive calculation on the init? We could remove the method from the useState initializer, but since the JSX renders before the useEffect callback, this can cause other problems, and will add plenty of optional chaining and other conditions and code smell.

const SomeComponent = ({textObject}) => {
    const  initMethod = (textObject)=>{
        return {...textObject, metaData:{length : textObject?.text?.length}};
    };

    const [someObject, setSomeObject] = React.useState();

    React.useEffect(()=>{
        setSomeObject(initMethod(textObject));
    }, [textObject]);

    
    if(!someObject) return null;
    
    return <div>
        {someObject.text} ({someObject.metaData?.length})
    </div>;
}

The solution

Unfortunately, the only solution I found for both cases is to wrap the useEffect with some hook that will prevent the useEffect first execution.

const useInitializedEffect = ((cb, dep) => {
    const [is] = React.useState({ loaded: false })

    React.useEffect(() => {
        if(is.loaded){
            cb();
        }else{
           is.loaded = true;
        }
    }, [...dep])
})

Now we can keep the initMethod in the useState, knowing that it will be executed only when it’s supposed to with all the expected data, and without the overhead of safety-net code.

const SomeComponent = ({textObject}) => {
    const  initMethod = (textObject)=>{
        return {...textObject, metaData:{length : textObject.text.length}};
    };

    const [someObject, setSomeObject] = React.useState(initMethod(textObject));

    useInitializedEffect(()=>{
        setSomeObject(initMethod(textObject));
    }, [textObject]);

    return <div>
        {someObject.text} ({someObject.metaData.length})
    </div>;
}

Conclusion

Nice? not! but useful.