How can I write generic react component wrappers - reactjs

I have a set of functional components that use a common set of properties, for ex:
const A = ({ x, y, z }) = {...}
const B = ({ x, y, z }) = {...}
I have partially fixed configurations for these components:
const styles {
A: {
type1: {
x: 1,
y: "something",
},
},
B: {
type1: {
x: 2,
y: "else",
},
},
};
To use these configurations, I've been writing a fixed component for each one:
// i have SFCs for A1, A2, B1, B2, C1, C2, ... etc.
const A1 = (props) = {
return <A x={styles.A.type1.x} y={styles.A.type1.y} {...props} />
}
...
// inside my business logic component
return (
<A1 z="state" />
)
This has been ok for a small amount of components and partial prop sets. However, going forward I'd like to be able to do something like this:
type A1 = styledType(styles.A.type1)(A);
return <A1 z="some_state" />
Do I need to define styledType in a type script file? Is mixing TS with JSX in a default create-react-app code base OK?
Can I refer to types (without instances) in JSX? Is what I just wrote above valid?
If there is something that already does this (react or even just plain JS / JSX / TS / whatever), I'm all ears!
So far this is what I've been playing with this:
const styledTyped = (styles) => {
return (component: C) => <C {...styles} />; // ????
};
export default styledTyped;
I know I need to return a callable that takes a component type, and returns a type wrapper that would have those properties injected.
I have no idea how to do that though... I'm currently studying material UI's withStyles, but it's got a lot going on and is a bit hard to follow as someone without experience in these languages.
EDIT:
I've gotten closer after looking at the implementation of withStyles in #material-ui/styles/withStyles/withStyles.js. I have something like this:
const withElementStyles = (styleProps) => {
return (Component) => {
return (props) => {
const newElement = React.createElement(Component, { ...props, ...styleProps });
return newElement;
};
};
};
This is almost working. It does indeed create the component with the fixed properties, as well as the ones I pass to the wrapper. However, I seem to have lost instance.type.name attribute, something I was relying on to perform type based dispatch of a few function calls.

I've found a solution (after learning about defaultProps), but also had to adjust other code (for the better).
I now have this:
const withStyledElement = (fixedProps) => {
return (Component) => {
const StyledElement = (props) => {
return <Component {...props} />;
};
StyledElement.defaultProps = fixedProps;
StyledElement.displayName = Component.name;
StyledElement.getConnector = Component.getConnector; // see below
return StyledElement;
};
};
I mentioned above that I was using the component name to perform type dispatch. This failed when I used a wrapped component like:
const WrappedA = withElementStyles(styles.A.1)(A);
because the component name is "StyledElement".
I decided that relying on the name was not a good approach. At the time I didn't realize you could bind functions to components after defining them.
So instead of dispatching on name, I use an expected bound method:
const getConnector = (instance, arg) => {
return instance.getConnector(instance, arg);
}
Now I don't have to add cases for each type, and I can create one-liner wrapped components that for all the flavors I need.

Related

Context provider not updating default value

I'm having some trouble modifying the default value given by a context. Below is a heavily simplified code, which still leads to the issue: As seen in the provider, I want greetWorld to become true, thus exhibiting "Hello world" instead of "..."
index.tsx:
import useSample, { SampleProvider } from './useSample'
const Example = () => {
const { greetWorld } = useSample()
return (
<SampleProvider>
{greetWorld ? 'Hello world' : '...'}
</SampleProvider>
)
}
useSample.tsx
import React from 'react'
type SampleContextType = {
greetWorld: boolean
}
const SampleContext = React.createContext<SampleContextType>({
greetWorld: false
})
export const SampleProvider = ({ children }: { children: React.ReactNode }) => {
const [greetWorld, setGreetWorld] = React.useState(true)
const value = React.useMemo(() => ({
greetWorld,
setGreetWorld
}), [greetWorld, setGreetWorld])
return <SampleContext.Provider value={value}>{ children }</SampleContext.Provider>
}
const useSample = () => {
const { greetWorld } = React.useContext(SampleContext)
return { greetWorld }
}
export default useSample
From my current understanding, greetWorld in index.tsx would get its value based on useSample's greetWorld, which in turn would be the greetWorld value given in SampleProvider, which is true. I've tried logging the greetWorld inside SampleProvider, and it shows true, so I'm assuming that SampleProvider is being reached properly, but I have no idea why things aren't being updated.
Regarding similar issues, this seemed rather similar, but in my simplified code there's no tag order to respect in the first place, so it can't be that, and this also seemed a little like my problem, but from what I can see, it seems like the consumer is the child already.
I get the feeling that the solution is rather obvious, as I'm unfamiliar with context hooks, but I wasn't able to find it. On a side note, since I'm also unfamiliar with memoization, I left it there, as it could be among the causes of the problem, but I also tried removing it and the problem persisted.

Expose state and method of child Component in parent with React

I know it's not a good pattern to do that, but you will understand why I want to do like that.
I have a HTable, which use a third-party library (react-table)
const HTable = <T extends object>({ columns, data, tableInstance}: Props<T>) {
const instance: TableInstance<T> = useTable<T> (
// Parameters
)
React.useImperativeHandle(tableInstance, () => instance);
}
Now, I want to control columns visibility from parent. I did:
const Parent = () => {
const [tableInstance, setTableInstance] = React.useState<TableInstance<SaleItem>>();
<Table data={data} columns={columns} tableInstance={(instance) => setTableInstance(instance)}
return tableInstance.columns.map((column) => {
<Toggle active={column.isVisible} onClick={() =>column.toggleHiden()}
}
}
The column hides well, but the state doesn't update and neither does the toggle, and I don't understand why. Could you help me to understand?
EDIT:
Adding a sandbox.
https://codesandbox.io/s/react-table-imperative-ref-forked-dilx3?file=/src/App.js
Please note that I cannot use React.forwardRef, because I use typescript and React.forwardRef doesn't allow generic type like this if I use forwardRef
interface TableProps<T extends object> {
data: T[],
columns: Column<T>[],
tableInstance?: React.RefObject<TableInstance<T>>,
}
Your issue is that react-tables useTable() hook always returns the same object as instance wrapper (the ref never changes). So your parent, is re-setting tableInstance to the same object - which does not trigger an update. Actually most of the contained values are also memoized. To get it reactive grab the headerGroups property.
const {
headerGroups,
...otherProperties,
} = instance;
React.useImperativeHandle(
tableInstance,
() => ({ ...properties }), // select properties individually
[headerGroups, ...properties],
);

Create Dynamic Components

I want to dynamically create a component, when I implement something like this:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = () => {
const Comp = gen_Comp("Hello");
return (
<Comp txt="World" />
);
}
Something goes wrong (what exactly goes wrong is hard to explain because it's specific to my app, point is that I must be doing something wrong, because I seem to be losing state as my component gets rerendered). I also tried this with React.createElement, but the problem remains.
So, what is the proper way to create components at runtime?
The main way that react tells whether it needs to mount/unmount components is by the component type (the second way is keys). Every time App renders, you call gen_Comp and create a new type of component. It may have the same functionality as the previous one, but it's a new component and so react is forced to unmount the instance of the old component type and mount one of the new type.
You need to create your component types just once. If you can, i recommend you use your factory outside of rendering, so it runs just when the module loads:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const Comp = gen_Comp("Hello");
const App = () => {
return (
<Comp txt="World" />
);
}
If it absolutely needs to be done inside the rendering of a component (say, it depends on props), then you will need to memoize it:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = ({ spec }) => {
const Comp = useMemo(() => {
return gen_Comp(spec);
}, [spec]);
return (
<Comp txt="World" />
);
}

Is separating data retrieval code into interfaces and impl. of those interfaces a good idea in React?

My question is: is the below pattern a good idea in React or no? I come from Java world where this type of code is standard. However, I've ran into several things that, while being a good idea in Java, are NOT a good idea in ReactJS. So I want to make sure that this type of code structure does not have weird memory leaks or hidden side-effects in the react world.
Some notes on below code: I'm only putting everything in the same file for brevity purposes. In real life, the react component the interface and the class would all be in their own source files.
What I'm trying to do: 1) Separate the display logic from data access logic so that my display classes are not married to a specific implementation of talking to a database. 2) Separating DAO stuff into interface + class so that I can later use a different type of database by replacing the class implementaton of the same DAO and won't need to touch much of the rest of the code.
so, A) Is this a good idea in React? B) What sort of things should I watch out for with this type of design? and C) Are there better patterns in React for this that I'm not aware of?
Thanks!
import { useState, useEffect } from 'react';
interface Dao {
getThing: (id: string) => Promise<string>
}
class DaoSpecificImpl implements Dao {
tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
}
getThing = async (id: string) => {
// use a specific database like firebase to
// get data from tabled called tablename
return "herp";
}
}
const dao: Dao = new DaoSpecificImpl("thingies");
const Display: React.FC = () => {
const [thing, setThing] = useState("derp");
useEffect(() => {
dao.getThing("123").then((newThing) =>
setThing(newThing));
});
return (
<div>{thing}</div>
)
}
export default Display;
https://codesandbox.io/s/competent-taussig-g948n?file=/src/App.tsx
The DaoSpecificImpl approach works however I would change your component to use a React hook:
export const useDAO = (initialId = "123") => {
const [thing, setThing] = useState("derp");
const [id, setId] = useState(initialId);
useEffect(() => {
const fetchThing = async () => {
try{
const data = await dao.getThing(id);
setThing(data);
}catch(e){
// Handle errors...
}
}
fetchThing();
}, [id]);
return {thing, setId};
}
using the hook in your component:
const Display = () => {
const {thing, setId} = useDao("123"); // If you don't specify initialId it'll be "123"
return <button onClick={() => setId("234")}>{thing}</button> // Pressing the button will update "thing"
}
Side note: You could also use a HOC:
const withDAO = (WrappedComponent, initialId = "123") => {
.... data logic...
return (props) => <WrappedComponent {...props} thing={thing} setId={setId}/>
};
export default withDAO;
E.g. using the HOC to wrap a component:
export default withDao(Display); // If you don't specify initialId it'll be "123"

React & Deck.GL: Add default props to each child component

I'm working on a configurable set of map layers with Deck.GL & React. I have a BaseMap component that I'll pass layers of data to as react children.
Currently, I have this:
BaseMap:
export const BaseMap = ({ latitude = 0, longitude = 0, zoom = 4, children }) => {
const deckProps = {
initialViewState: { latitude, longitude, zoom },
controller: true
};
return (
<DeckGL {...deckProps}>
{children}
<StaticMap />
</DeckGL>
);
};
And it's used like this:
<BaseMap>
<ScatterplotLayer
data={scatterData}
getPosition={getPositionFn}
getRadius={1}
radiusUnits={'pixels'}
radiusMinPixels={1}
radiusMaxPixels={100}
filled={true}
getFillColor={[255, 255, 255]}
/>
<TextLayer
data={textData}
getPosition={getPositionFn}
getColor={[255, 0, 0]}
getText={getTextFn}
/>
</BaseMap>
This is okay, but I want to add default props to each child.
Attempt 1
I've tried this in BaseMap, but get the error cannot assign to read only property props of object #<Object>:
...
return (
<DeckGL {...deckProps}>
{React.Children.map(children, (c) => {
const defaultProps = {
loaders: [CSVLoader]
}
c.props = { ...defaultProps, ...c.props };
return c;
})}
</DeckGL>
);
Attempt 2
I've also tried creating a wrapper component for each type of layer, but get the error Cannot call a class as a function:
wrapper:
export const ScatterplotLayerWrapper = (props) => {
const defaultScatterProps = {
loaders: [CSVLoader]
};
const scatterLayerProps = {
...defaultScatterProps,
...props
};
return <ScatterplotLayer {...scatterLayerProps} />;
};
used like this:
<BaseMap>
<ScatterplotLayerWrapper
data={scatterData}
getPosition={getPositionFn}
/>
</BaseMap>
I suspect the problem with this second attempt has something to do with the caveat here.
Solution?
I can imagine two types of solutions (and obviously, there may be others!):
correct method for checking the layer type & modifying child props depending on the type, or something similar - is this possible?
or
Some way to convince react/deck.gl that ScatterplotLayer will be a child of Deck.GL, even if it isn't in ScatterplotLayerWrapper. (This one seems less likely)
The confusion came from a mis-understanding of how deck.gl's React component works & what those <ScatterplotLayer> components really are (they're not react components).
Deck.gl's react component, DeckGL, intercepts all children and determines if they are in fact "layers masquerading as react elements" (see code). It then builds layers from each of those "elements" and passes them back to DeckGL's layers property.
They look like react components, but really aren't. They can't be rendered on their own in a React context. They can't be rendered outside of the DeckGL component at all, because they're still just plain deck.gl layers.
The solution here is to create a new map layer class just like you might in any other context (not a React component wrapping a layer). Docs for that are here.
class WrappedTextLayer extends CompositeLayer {
renderLayers() { // a method of `Layer` classes
// special logic here
return [new TextLayer(this.props)];
}
}
WrappedTextLayer.layerName = 'WrappedTextLayer';
WrappedTextLayer.defaultProps = {
getText: (): string => 'x',
getSize: (): number => 32,
getColor: [255, 255, 255]
};
export { WrappedTextLayer };
This new layer can then be used in the BaseMap component (or the un-wrapped DeckGL component`) like this:
<BaseMap>
<WrappedTextLayer
data={dataUrl}
getPosition={(d) => [d.longitude, d.latitude]}
/>
</BaseMap>
In addition, the exact same layer can be passed to DeckGL as a layer prop:
<DeckGL
layers={[
new WrappedTextLayer({
data: dataUrl,
getPosition: (d) => [d.longitude, d.latitude]
})
]}
></DeckGL>
Modifying the BaseMap component a little will allow it to accept layers either as JSX-like children, or via the layers prop as well:
export const BaseMap = ({ latitude = 0, longitude = 0, zoom = 4, children, layers }) => {
const deckProps = {
initialViewState: { latitude, longitude, zoom },
controller: true,
layers
};
return (
<DeckGL {...deckProps}>
{children && !layers ? children : null}
<StaticMap />
</DeckGL>
);
};

Resources