rn-dynamic-ui-render
Version:
A dynamic UI rendering engine for React Native
155 lines (135 loc) • 4.34 kB
JavaScript
import React, { createContext, useContext } from "react";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { ErrorBoundary } from "./utils";
import {
handleColor,
handleDynamicData,
handleStyles,
handleVisible,
handleInteractions,
evaluateCondition,
} from "./utils/handlers";
// Create a Context for components and theme
const UIContext = createContext({
components: {},
theme: {},
});
/**
* Hook to get the components and theme from the context. This hook will
* throw an error if the components and theme are not provided.
*/
const useUIContext = () => {
const context = useContext(UIContext);
if (!context.components || !context.theme) {
throw new Error(
"RNDynamicUIRender must be provided with components and theme"
);
}
return context;
};
/**
* Renders a React component based on the provided configuration object.
*/
const renderComponent = (e, index, handlers, t) => {
const { components, theme } = useUIContext();
const Component = components[e.name];
if (!Component) {
console.warn(`Component ${e.name} not found`);
return null;
}
const props = e.props || {};
const componentProps = { ...props };
handleStyles(props, componentProps, theme, handlers);
handleDynamicData(props, componentProps, handlers);
handleColor(props, theme, componentProps);
handleInteractions(props, handlers, componentProps);
switch (e.name) {
case "FlatList":
componentProps.keyExtractor = (item, index) =>
item.id?.toString() || index.toString();
componentProps.ListFooterComponent = () => (
<RNDynamicUIRender data={e.listFooterComponent} handlers={handlers} />
);
componentProps.ListEmptyComponent = () => (
<RNDynamicUIRender data={e.listEmptyComponent} handlers={handlers} />
);
componentProps.ListHeaderComponent = () => (
<RNDynamicUIRender data={e.listHeaderComponent} handlers={handlers} />
);
componentProps.renderItem = ({ item, index }) => (
<RNDynamicUIRender
data={e.content}
handlers={{ ...handlers, item, index }}
/>
);
break;
default:
break;
}
const content = Array.isArray(e?.content) ? (
<RNDynamicUIRender data={e.content} handlers={handlers} />
) : (
t(componentProps.textContent || "") || null
);
const isVisible =
handleVisible(props, handlers) &&
evaluateCondition(props?.if || true, handlers, handlers?.item);
if (isVisible) {
return (
<Component {...componentProps} key={index}>
{content}
</Component>
);
}
return null;
};
const ComponentRenderer = React.memo(({ data, handlers, t }) => {
if (!Array.isArray(data)) return null;
return data.map((e, i) => renderComponent(e, i, handlers, t));
});
/**
* RNDynamicUIRender is a component that combines the UIContext provider
* and recursive component rendering. It accepts `components`, `theme`, and
* optional translation and renders the UI based on the passed schema (`data`).
*/
const RNDynamicUIRender = ({
data,
handlers,
theme,
components,
translate = false,
}) => {
const contextValue = useContext(UIContext);
const effectiveComponents = components ?? contextValue.components;
const effectiveTheme = theme ?? contextValue.theme;
if (!effectiveComponents || !effectiveTheme) {
throw new Error(
"RNDynamicUIRender must be provided with components and theme"
);
}
const { t } = translate ? useTranslation() : { t: (str) => str };
const content = (
<ErrorBoundary>
<ComponentRenderer data={data} handlers={handlers} t={t} />
</ErrorBoundary>
);
// Only wrap with a new provider if new values were passed in
return components || theme ? (
<UIContext.Provider
value={{ components: effectiveComponents, theme: effectiveTheme }}
>
{content}
</UIContext.Provider>
) : (
content
);
};
RNDynamicUIRender.propTypes = {
data: PropTypes.array,
handlers: PropTypes.object,
theme: PropTypes.object,
components: PropTypes.object,
translate: PropTypes.bool,
};
export default RNDynamicUIRender;