typescriptdomastrojspreact

Traversing and Transforming Children of preact components


I've recently had a problem with needing to traverse and transform some DOM children from a Parent Preact component:

import {isElevated} from "../stores/UserStateStore.ts";
import type {elevatedUserList} from "../../../plugins/remark/util/remarkOfWikiLinks-utils.ts";
import {Children} from "preact/compat";

interface RedactionProps {
    level: number,
    children: any;
}

function redactContent(secinfo: elevatedUserList, level: number, content: any): any {
    return Children.map(content, (child: any): any => {
        if (typeof child === 'string') {
            return isElevated(secinfo, level, true) ? child : child.replace(/\S/g, '█');
        } else if (child.props && child.props.children) {

            const {children, ...rest} = child.props;

            return {
                ...rest,
                props: {
                    ...child.props,
                    children: redactContent(secinfo, level, child.props.children)
                }
            };
        }
        return child;
    });
}

export function Redaction(props: RedactionProps) {

    // find metadata element from document root with id "secinfo"
    const secinfo = document.getElementById('secinfo');
    const elevatedUsers = JSON.parse(secinfo?.getAttribute('data-elevatedUsers') || '{}')


    return (
        <div class="redaction" data-level={props.level}>
            {redactContent(elevatedUsers, props.level, props.children)}
        </div>
    )
}

The intention of the Component is to determine if it's children should be redacted or not based on some user credentials.

What I have here does the trick of traversing the children passed as props, though it also renders the original content below it.

I can't quite seem to find the issue with my code. Also, is there any form that's more maintainable or standard than my approach?


Solution

  • The issue is the second branch of your conditional: ...rest is not what you want, as it's child.props excluding children, you're removing all the top-level properties by accident. Spreads are awful for perf though and there's no reason to be creating a new child object anyways: just mutate .props.children and return the original child object.

    I'd also suggest using toChildArray from core rather than Children from preact/compat:

    import { toChildArray } from 'preact';
    
    function redactContent(secinfo: elevatedUserList, level: number, content: any): any {
        return toChildArray(content).map(child: any): any => {
            if (typeof child === 'string') {
                return isElevated(secinfo, level, true) ? child : child.replace(/\S/g, '█');
            }
            
            if (child.props && child.props.children) {
                child.props.children = redactContent(secinfo, level, child.props.children);
                return child;
            }
    
            return child;
        });
    }