Array of JSX Elements not rendering on second loop - solid-js

I have a Solid.js code that looks like this:
import { render, For } from "solid-js/web";
const Text = () => <div style="color: red">Example</div>;
const App = () => {
const elements = [<Text/>, <Text/>, <Text/>];
return (
<div>
<div>First For Each</div>
<For each={elements}>{(E) => E}</For>
<div>Second For Each</div>
<For each={elements}>{(E) => E}</For>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
But for some reason Solid.js only renders the second <For>:
And when I change the elements to:
const elements = [() => <Text/>, () => <Text/>, () => <Text/>];
it renders twice (also works fine if I change the elements value to primitive value like int or string. Can someone explain to me why Solid.js behaves this way?
Playground Example

Writing <Text/> executes the Text component which returns an actual DOM node. And dom nodes can be inserted only in one place in the DOM.
The attempt with wrapping the component execution with functions works because you'll get a different element instance every time you execute that function. You're basically creating an array of components vs an array of HTML elements.
Here is a similar Github issue: https://github.com/solidjs/solid/issues/899

Related

"jQuery for React AST"?

Is there such a thing as a "jQuery for React AST"? That like jQuery allows for elegant search, traversal, creation, mutation of an AST that contains things like JSXNode, etc? I saw that acorn has some basic traversal stuff, but it isn't super usable for repeatedly doing reorders, insertions, wrapping a component in {flag && } to conditionally render, etc, etc. I'm not even sure how to google for this except "jquery for AST" which, uh yeah, didn't work.
Subsecond
You can use Subsecond for this purpose, here is how it will looks like:
Input:
<h1>hello world</h1>
Output:
<h2>hello world</h2>
Transformation:
S('JSXIdentifier').each((node) => {
node.text('h2');
})
πŸ“œ
Here is playground.
🐊Putout
Also there is a way to do similar things using declarative approach with help of 🐊Putout I'm working on:
Change tag
Input:
<h1>hello world</h1>
Output:
<h2>hello world</h2>
Transformation:
export const replace = () => ({
'<h1>__a</h1>': '<h2>__a</h2>',
});
πŸŠπŸ“œ
Here is playground.
Change Attribute
You can also change an attribute className to class:
Input:
const div = <div className="abc">{x}</div>
Output:
const div = <div class="abc">{x}</div>
Transformation:
export const replace = () => ({
'<div className="__a">__jsx_children</div>': '<div class="__a">__jsx_children</div>',
});
πŸŠπŸ“œ
Here is playground

Dynamic tag name in Solid JSX

I would like to set JSX tag names dynamically in SolidJS. I come from React where it is fairly simple to do:
/* Working ReactJS Code: */
export default MyWrapper = ({ children, ..attributes }) => {
const Element = "div";
return (
<Element {...attributes}>
{children}
</Element>
)
}
but when I try to do the same thing in SolidJS, I get the following error:
/* Console output when trying to do the same in SolidJS:Β */
dev.js:530 Uncaught (in promise) TypeError: Comp is not a function
at dev.js:530:12
at untrack (dev.js:436:12)
at Object.fn (dev.js:526:37)
at runComputation (dev.js:706:22)
at updateComputation (dev.js:691:3)
at devComponent (dev.js:537:3)
at createComponent (dev.js:1236:10)
at get children [as children] (Input.jsx:38:5)
at _Hot$$Label (Input.jsx:7:24)
at #solid-refresh:10:42
I would like to know if I miss something here, or whether it is possible to achieve this in SolidJS in any other way.
Solid has a <Dynamic> helper component for that use.
import {Dynamic} from "solid-js/web";
<Dynamic component="div" {...attributes}>
{props.children}
</Dynamic>
Here is an alternative implementation covering simple cases like strings and nodes although you can extend it to cover any JSX element:
import { Component, JSXElement} from 'solid-js';
import { render, } from 'solid-js/web';
const Dynamic: Component<{ tag: string, children: string | Node }> = (props) => {
const el = document.createElement(props.tag);
createEffect(() => {
if(typeof props.children === 'string') {
el.innerText = String(props.children);
} else if (props.children instanceof Node){
el.appendChild(props.children);
} else {
throw Error('Not implemented');
}
});
return el;
};
const App = () => {
return (
<div>
<Dynamic tag="h2">This is an H2!</Dynamic>
<Dynamic tag="p">This is a paragraph!</Dynamic>
<Dynamic tag="div"><div>Some div element rendering another div</div></Dynamic>
</div>
)
}
render(App, document.body);
This works because Solid components are compiled into native DOM elements, however since we do not escape the output, it is dangerous to render any children directly, given that you have no control over the content.
This alternative comes handy when you need to render rich text from a content editable or a textarea, text that includes tags like em, strong etc. Just make sure you use innerHTML attribute instead of innerText.

SolidJS: For in child component not rendering list

I have a parent component returning:
<List list={list()}>
{(item, index) => <div>{item}</div>}
</List>
where list is a created signal. List is a custom component I made returning:
<div>
<For each={list}>{children}</For>
</div>
but whenever list is updated, it isn't rendered. When I move the For code to the parent, it renders, so what is it about passing a signal's value to a child component that makes it not rerender on updates?
EDIT: demo
import { render } from "solid-js/web";
import { createSignal, For } from "solid-js";
function Counter() {
const [list, setList] = createSignal([]);
const increment = () => setList([...list(), 1]);
return (
<>
<button type="button" onClick={increment}>
add
</button>
<p>broken list</p>
<List list={list()} />
<p>other list</p>
<For each={list()}>
{(item) => <p>{item}</p>}
</For>
</>
);
}
function List({list}) {
return (
<For each={list}>
{(item) => <p>{item}</p>}
</For>
);
}
render(() => <Counter />, document.getElementById("app"));
EDIT 2: I meant to use <List list={list()} />, which also doesn't work, but I missed it earlier.
It does not work because destructuring props in Solid loses reactivity, that is, all the destructured props values do not update, ever.
Destructuring props is sometimes convenient and commonly used in other frameworks, but is not really recommended in Solid - the FAQ says:
By destructuring, you separate the value from the object, giving you the value at that point in time and losing reactivity.
You need to rewrite List component to use single props parameter and access props.list in the JSX:
function List(props) {
return (
<For each={props.list}>
{(item) => <p>{item}</p>}
</For>
);
}
Why destructuring does not work? In Solid, props is an object, created behind the scenes by Solid, with getters for intercepting access to each individual property, like props.something. It's needed to track JSX (expressions and fragments) and effects (created by createEffect()) so that they are reevaluated and updated when props.something changes. There's no way to track access to properties which are destructured (well there's plugin for that, but it's not in the core framework because it has some overhead).

How to handle state in Material UI Data Grid's components

I'm attempting to add search functionality to a data grid component. In order to achieve this, I'm adding an input element to the table using the components.header prop as follows (I've omitted irrelevant code):
const Table = () => {
const filterRows = (rows) => {
return rows;
};
const [searchTerm, setSearchTerm] = useState("");
const Header = () => (
<input
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
/>
);
return (
<div style={{ height: 500 }}>
<DataGrid
rows={searchTerm ? filterRows(orders) : orders}
columns={orderColumns}
components={{
header: () => <Header />
}}
/>
</div>
);
};
The issue I'm having is that the input element loses focus each time a character is entered into the input in the header. Presumably, this is because it updates state, which triggers a re-render. This makes it impossible to share and access state of anything contained inside the Data Grid's components because it requires a React.FC argument and won't accept a ReactElement so the input is always re-rendered.
Am I missing something or is this actually not possible with Material UI's Data Grid? It seems like a pretty expected use-case to have something stateful in the header that we'd want to access like a controlled input component in order to use it as a kind of "Tool bar" as mentioned in the Material UI docs.
I've created a code sandbox to replicate the issue here: https://codesandbox.io/s/compassionate-keldysh-z995k?file=/src/App.js:246-726.
Cheers.

Extract function to standalone custom React component in react-leaflet

My primary goal is to call fitBounds whenever a FeatureGroup is rendered in react-leaflet on initial load.
This renders correctly -
<Map>
<LayersControl>
{getLayers(groups)}
</LayersControl>
</Map>
function getLayers(featureGroups: MyFeatureGroup[]){
const showOnLoad = true;
return featureGroups.map((group: MyFeatureGroup) => {
const groupRef = createRef<FeatureGroup>();
const { id, name, } = group;
return (
<LayersControl.Overlay checked={showOnLoad} key={id} name={name}>
<FeatureGroup ref={groupRef}>
<Layer {...group} />
</FeatureGroup>
</LayersControl.Overlay>
);
});
}
However, because it is using a function instead of React component, I don't have access to using React hooks.
The alternative that I tried does not work, even though it is the same code wrapped in a React component -
...same as above...
return featureGroups.map((group: MyFeatureGroup) => (
<ControlledGroup {...group} showOnLoad={showOnLoad} /> ///----- > ADDED THIS HERE
));
const ControlledGroup: React.FC<ControlledGroupProps> = (props) => {
const groupRef = createRef<FeatureGroup>();
const { map } = useLeaflet();
/// -----> map is correctly defined here - injecting to all of the layers (LayersControl, FeatureGroup) does not solve the problem
const { showOnLoad, ...group } = props;
useEffect(() => fitBounds(map, groupRef)); ///-----> Primary Goal of what I am trying to accomplish
return (
<LayersControl.Overlay
checked={showOnLoad}
key={group.id}
name={name}
>
<FeatureGroup ref={groupRef}>
<Layer map={map} {...group} />
</FeatureGroup>
</LayersControl.Overlay>
);
};
I am a bit stumped, since this is the same code. The getLayers function returns a ReactNode in both cases. However, when moving to a standalone ControlledGroup component, it throws an error on render -
addOverlay is not a function
I tried creating a custom class component for react-leaflet, but the difficulty that I ran into there is that createLeafletElement returns a Leaflet.Element, whereas I am simply looking to return a ReactNode. That is, all of these are valid react-leaflet components already.
My questions - why does one work and the other does not? What is the correct/recommended way to convert this function to a renderable stand-alone React component?
Further, if there is an alternative pattern to calling fitBounds, that would be helpful as well.
Any insight would be appreciated.
Since the Layers share an inheritance with Layers.Overlay, the solution to the render error is to keep the Layers together and move the feature group to a standalone component.
This works as expected and allows me to call useEffect on the groupRef -
function getLayers(groups: MyFeatureGroup[]){
return featureGroups.map((group: MyFeatureGroup) => {
const { id, name, } = group;
return (
///---> Keep the Overlay in the function here and extract just the FeatureGroup out
<LayersControl.Overlay checked={showOnLoad} key={id} name={name}>
<ControlledGroup {...group}></ControlledGroup>
</LayersControl.Overlay>
);
}