rn-dynamic-ui-render
Version:
A dynamic UI rendering engine for React Native
201 lines (178 loc) • 5.18 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: {},
});
/**
* Renders a React component based on the provided configuration object.
*/
const renderComponent = (
e,
index,
handlers,
t,
translate,
components,
theme
) => {
if (!e || !e.name) return null;
const Component = components[e.name];
if (!Component) {
if (__DEV__) console.warn(`Component ${e.name} not found`);
return null;
}
const props = e.props || {};
const componentProps = { ...props };
// Apply handlers
handleStyles(props, componentProps, theme, handlers);
handleDynamicData(props, componentProps, handlers);
handleColor(props, theme, componentProps);
handleInteractions(props, handlers, componentProps);
// Special case for FlatList
if (e.name === "FlatList") {
componentProps.keyExtractor = (item, idx) =>
item?.id?.toString() || idx.toString();
componentProps.ListFooterComponent = () => (
<RNDynamicUIRender
data={e.listFooterComponent}
handlers={handlers}
translate={translate}
theme={theme}
components={components}
/>
);
componentProps.ListEmptyComponent = () => (
<RNDynamicUIRender
data={e.listEmptyComponent}
handlers={handlers}
translate={translate}
theme={theme}
components={components}
/>
);
componentProps.ListHeaderComponent = () => (
<RNDynamicUIRender
data={e.listHeaderComponent}
handlers={handlers}
translate={translate}
theme={theme}
components={components}
/>
);
componentProps.renderItem = ({ item, index: itemIndex }) => (
<RNDynamicUIRender
data={e.content}
handlers={{ ...handlers, item, index: itemIndex }}
translate={translate}
theme={theme}
components={components}
/>
);
}
// Determine content
const content = Array.isArray(e?.content) ? (
<RNDynamicUIRender
data={e.content}
handlers={handlers}
translate={translate}
theme={theme}
components={components}
/>
) : t != null ? (
t(componentProps.textContent || "") || null
) : (
componentProps.textContent || null
);
// Visibility check
const isVisible =
handleVisible(props, handlers) &&
evaluateCondition(props?.if || true, handlers, handlers?.item);
if (!isVisible) return null;
return (
<Component {...componentProps} key={index}>
{content}
</Component>
);
};
/**
* ComponentRenderer is memoized for performance.
*/
const ComponentRenderer = React.memo(
({ data, handlers, t, translate, components, theme }) => {
if (!Array.isArray(data)) return null;
return data.map((e, i) =>
renderComponent(e, i, handlers, t, translate, components, theme)
);
}
);
/**
* Renders a UI from a JSON schema.
*
* @param {{ data: array, handlers: object, theme: object, components: object, translate: boolean }} props
* @property {array} data - The JSON schema defining the UI
* @property {object} handlers - Data and logic context
* @property {object} theme - Design tokens for styling
* @property {object} components - Custom component registry
* @property {boolean} translate - Enable translations via react-i18next
* @returns {React.ReactElement}
*/
const RNDynamicUIRender = ({
data,
handlers,
theme,
components,
translate = false,
}) => {
const { t: translateFunction } = useTranslation();
const t = translate == true ? translateFunction : (str) => str;
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 content = (
<ErrorBoundary>
<ComponentRenderer
data={data}
handlers={handlers}
t={t}
translate={translate}
components={effectiveComponents}
theme={effectiveTheme}
/>
</ErrorBoundary>
);
// Only wrap in provider if new values are passed
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;