Is there an optimal way to write dynamic component switching in React? - dynamic

When writing components for a layout that needs to be switched dynamically via data from the backend, I often find myself writing React components that look like this:
import React from 'react';
import TextInput from './TextInput';
import DateInput from './DateInput';
const Input = (props) => {
const {
type,
...otherProps
} = props;
switch (type) {
case 'text':
return <TextInput {...otherProps} />;
case 'date':
return <DateInput {...otherProps} />;
// etc…
default:
return null;
}
};
export default Input;
Which leads to the list of imports ballooning when the types are expanded upon.
Is there any alternative method for dynamic component switching that would be more optimal/performant/reliable than this one?

How about this:
import React from 'react';
import TextInput from './TextInput';
import DateInput from './DateInput';
const TYPES = {
text: TextInput,
date: DateInput,
};
const Input = (props) => {
const {
type,
...otherProps
} = props;
const Component = TYPES[type];
if (!Component) return null;
return <Component {...otherProps} />;
};
export default Input;
Generally you want to enumerate the possible options somewhere, and an object lookup is an easy way to do it. Dynamic require calls that other answers have mentioned are generally a little questionable because tools cannot analyse the dependencies, and it means you're API is much harder to understand.

If you use a build tool such as webpack dynamic requires are supported, which allows you to do something like the following:
import React from 'react';
const typeMap = {
text: 'TextInput',
date: 'DateInput'
};
const Input = (props) => {
const {
type,
...otherProps
} = props;
const typeInput = typeMap[type];
if (!typeInput) return null;
const InputComponent = require(`./${typeInput}`);
return <InputComponent { ...otherProps } />;
};
export default Input;

You can use require to dynamically load modules. However you have to define somewhere what is the component module path. For example:
const components = {
text: 'TextInput',
date: 'DateInput',
};
const Input = (props) => {
const { type, ...otherProps } = props;
const Component = require('./' + components[type]);
return type ? <Component {...otherProps} /> : null;
};

Related

How can I use parent's callback in children component?

Sorry. I'm new to react native and react.
And I just encountered setCount is not a function. (In 'setCount(1)','setCount' is undefined) error.
How can I use setCount Method in AComponent?
import React, {useState} from 'react';
import {
Text,
} from 'react-native';
const AComponent = ({count, callback}) => {
callback(1);
return <Text>{count}</Text>;
};
const App = () => {
const [count, setCount] = useState(0);
return <AComponent count={count} callback={setCount} />;
};
const styles = StyleSheet.create({
container: {},
});
export default App;
You are renaming your function to callback.
So in your component you need to use callback instead of setCount, or rename your props callback to setCount (preferable):
return <AComponent count={count} setCount={setCount} />
Each key={value} defined when you use your component, can be accessed with the key in the component.
I suggest you to have a deep read of the following article: https://reactjs.org/docs/components-and-props.html

Can an independent functional component re-render based on the state change of another?

I'm new to React Native, and my understanding is that functional components and hooks are the way to go. What I'm trying to do I've boiled down to the simplest case I can think of, to use as an example. (I am, by the way, writing in TypeScript.)
I have two Independent components. There is no parent-child relationship between the two. Take a look:
The two components are a login button on the navigation bar and a switch in the enclosed screen. How can I make the login button be enabled when the switch is ON and disabled when the switch is OFF?
The login button looks like this:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const handleClick = () => {
navigation.navigate('Away');
};
// I want the 'disabled' value to update based on the state of the switch.
return (
<Button title="Login"
color="white"
disabled={false}
onPress={handleClick} />
);
};
As you can see, right now I've simply hard-coded the disabled setting for the button. I'm thinking that will no doubt change to something dynamic.
The screen containing the switch looks like this:
const HomeScreen = () => {
const [isEnabled, setEnabled] = useState(false);
const toggleSwitch = () => setEnabled(value => !value);
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</SafeAreaView>
);
};
What's throwing me for a loop is that the HomeScreen and LoginButton are setup like this in the navigator stack. I can think of no way to have the one "know" about the other:
<MainStack.Screen name="Home"
component={HomeScreen}
options={{title: "Home", headerRight: LoginButton}} />
I need to get the login button component to re-render when the state of the switch changes, but I cannot seem to trigger that. I've tried to apply several different things, all involving hooks of some kind. I have to confess, I think I'm missing at least the big picture and probably some finer details too.
I'm open to any suggestion, but really I'm wondering what the simplest, best-practice (or thereabouts) solution is. Can this be done purely with functional components? Do I have to introduce a class somewhere? Is there a "notification" of sorts (I come from native iOS development). I'd appreciate some help. Thank you.
I figured out another way of tracking state, for this simple example, that doesn't involve using a reducer, which I'm including here for documentation purposes in hopes that it may help someone. It tracks very close to the accepted answer.
First, we create both a custom hook for the context, and a context provider:
// FILE: switch-context.tsx
import React, { SetStateAction } from 'react';
type SwitchStateTuple = [boolean, React.Dispatch<SetStateAction<boolean>>];
const SwitchContext = React.createContext<SwitchStateTuple>(null!);
const useSwitchContext = (): SwitchStateTuple => {
const context = React.useContext(SwitchContext);
if (!context) {
throw new Error(`useSwitch must be used within a SwitchProvider.`);
}
return context;
};
const SwitchContextProvider = (props: object) => {
const [isOn, setOn] = React.useState(false);
const [value, setValue] = React.useMemo(() => [isOn, setOn], [isOn]);
return (<SwitchContext.Provider value={[value, setValue]} {...props} />);
};
export { SwitchContextProvider, useSwitchContext };
Then, in the main file, after importing the SwitchContextProvider and useSwitchContext hook, wrap the app's content in the context provider:
const App = () => {
return (
<SwitchContextProvider>
<NavigationContainer>
{MainStackScreen()}
</NavigationContainer>
</SwitchContextProvider>
);
};
Use the custom hook in the Home screen:
const HomeScreen = () => {
const [isOn, setOn] = useSwitchContext();
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={setOn}
value={isOn}
/>
</SafeAreaView>
);
};
And in the Login button component:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const [isOn] = useSwitchContext();
const handleClick = () => {
navigation.navigate('Away');
};
return (
<Button title="Login"
color="white"
disabled={!isOn}
onPress={handleClick} />
);
};
I created the above by adapting an example I found here:
https://kentcdodds.com/blog/application-state-management-with-react
The whole project is now up on GitHub, as a reference:
https://github.com/software-mariodiana/hellonavigate
If you want to choose the context method, you need to create a component first that creates our context:
import React, { createContext, useReducer, Dispatch } from 'react';
type ActionType = {type: 'TOGGLE_STATE'};
// Your initial switch state
const initialState = false;
// We are creating a reducer to handle our actions
const SwitchStateReducer = (state = initialState, action: ActionType) => {
switch(action.type){
// In this case we only have one action to toggle state, but you can add more
case 'TOGGLE_STATE':
return !state;
// Return the current state if the action type is not correct
default:
return state;
}
}
// We are creating a context using React's Context API
// This should be exported because we are going to import this context in order to access the state
export const SwitchStateContext = createContext<[boolean, Dispatch<ActionType>]>(null as any);
// And now we are creating a Provider component to pass our reducer to the context
const SwitchStateProvider: React.FC = ({children}) => {
// We are initializing our reducer with useReducer hook
const reducer = useReducer(SwitchStateReducer, initialState);
return (
<SwitchStateContext.Provider value={reducer}>
{children}
</SwitchStateContext.Provider>
)
}
export default SwitchStateProvider;
Then you need to wrap your header, your home screen and all other components/pages in this component. Basically you need to wrap your whole app content with this component.
<SwitchStateProvider>
<AppContent />
</SwitchStateProvider>
Then you need to use this context in your home screen component:
const HomeScreen = () => {
// useContext returns an array with two elements if used with useReducer.
// These elements are: first element is your current state, second element is a function to dispatch actions
const [switchState, dispatchSwitch] = useContext(SwitchStateContext);
const toggleSwitch = () => {
// Here, TOGGLE_STATE is the action name we have set in our reducer
dispatchSwitch({type: 'TOGGLE_STATE'})
}
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={switchState}
/>
</SafeAreaView>
);
};
And finally you need to use this context in your button component:
// We are going to use only the state, so i'm not including the dispatch action here.
const [switchState] = useContext(SwitchStateContext);
<Button title="Login"
color="white"
disabled={!switchState}
onPress={handleClick} />
Crete a reducer.js :
import {CLEAR_VALUE_ACTION, SET_VALUE_ACTION} from '../action'
const initialAppState = {
value: '',
};
export const reducer = (state = initialAppState, action) => {
if (action.type === SET_VALUE_ACTION) {
state.value = action.data
}else if(action.type===CLEAR_VALUE_ACTION){
state.value = ''
}
return {...state};
};
Then action.js:
export const SET_VALUE_ACTION = 'SET_VALUE_ACTION';
export const CLEAR_VALUE_ACTION = 'CLEAR_VALUE_ACTION';
export function setValueAction(data) {
return {type: SET_VALUE_ACTION, data};
}
export function clearValueAction() {
return {type: CLEAR_VALUE_ACTION}
}
In your components :
...
import {connect} from 'react-redux';
...
function ComponentA({cartItems, dispatch}) {
}
const mapStateToProps = (state) => {
return {
value: state.someState,
};
};
export default connect(mapStateToProps)(ComponentA);
You can create more components and communicate between them, independently.

Check the render method of `InstanceImage` while testing using testing-library/react-native

I am trying to test a functional component with Redux using testing-library/react-native.
//InstanceImage.component.js
export default InstanceImage = (props) => {
// <props init, code reduced >
const { deviceId, instanceId } = DeviceUtils.parseDeviceInstanceId(deviceInstanceId)
const deviceMap = useSelector(state => {
return CommonUtils.returnDefaultOnUndefined((state) => {
return state.deviceReducer.deviceMap
}, state, {})
})
const device = deviceMap[deviceId]
const instanceDetail = device.reported.instances[instanceId]
if (device.desired.instances[instanceId] !== undefined) {
return (
<View
height={height}
width={width}
>
<ActivityIndicator
testID={testID}
size={loadingIconSize}
color={fill}
/>
</View>
)
}
const CardImage = ImageUtils[instanceDetail.instanceImage] === undefined ? ImageUtils.custom : ImageUtils[instanceDetail.instanceImage]
return (
<CardImage
testID={testID}
height={height}
width={width}
fill={fill}
style={style}
/>
)
}
And the test file is
//InstanceImage.component.test.js
import React from 'react'
import { StyleSheet } from 'react-native';
import { color } from '../../../../src/utils/Color.utils';
import ConstantsUtils from '../../../../src/utils/Constants.utils';
import { default as Helper } from '../../../helpers/redux/device/UpdateDeviceInstanceIfLatestHelper';
import InstanceImage from '../../../../src/components/Switches/InstanceImage.component';
import { Provider } from 'react-redux';
import { render } from '#testing-library/react-native';
import configureMockStore from "redux-mock-store";
import TestConstants from '../../../../e2e/TestConstants';
const mockStore = configureMockStore();
const store = mockStore({
deviceReducer: {
deviceMap: Helper.getDeviceMap()
}
});
const currentState = ConstantsUtils.constant.OFF
const styles = StyleSheet.create({
// styles init
})
const props = {
//props init
}
describe('Render InstanceImage', () => {
it('InstanceImage renders correctly with values from props and Redux', () => {
const rendered = render(<Provider store={store}>
<InstanceImage {...props}/>
</Provider>)
const testComponent = rendered.getByTestId(TestConstants.icons.INSTANCE_IMAGE)
});
})
And when I run the test file it gives the following error
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Check the render method of `InstanceImage`.
The component runs correctly when the app is run and there aren't any crashes. Still, this error occurs while performing tests.I am new to creating test cases for react native app so not able to debug this issue.
Found the error, the CardImage returns a custom svg image which was causing the issue. I used the jest-svg-transformer and it solved the issue.
Reference: https://stackoverflow.com/a/63042067/4795837

test content of a Text element in a stateful component

I am using react-native-testing-library. My component is quite simple:
import React, {Component} from 'react';
import {Text, View} from 'react-native';
import {information} from './core/information';
export default class Logo extends Component {
constructor() {
super();
this.state = {
name: ''
};
information()
.then((details) => {
this.setState({
name: details['name']
});
})
.catch((e) => {
console.log(e);
});
}
render() {
return (
<>
<View>
<Text>{this.state.name}</Text>
</View>
</>
);
}
}
I want to make sure contains the right content. I tried the following but it is failing:
import * as info from "./lib/information";
it('displays correct text', () => {
const spy = jest.spyOn(info, 'information')
const data = {'name':'name'}
spy.mockResolvedValue(Promise.resolve(data));
const {queryByText, debug} = render(<Logo />);
expect(queryByText(data.name)).not.toBeNull();
expect(spy).toHaveBeenCalled();
});
I can confirm the function information() was spied on correctly but still debug(Logo) shows the Text element with empty string.
If it's correctly spying you can try this. I encourage you to use the testID props for the components
render() {
return (
<>
<View>
<Text testID="logo-text">{this.state.name}</Text>
</View>
</>
);
}
import * as info from "./lib/information";
import { waitForElement, render } from "react-native-testing-library";
it('displays correct text', () => {
const spy = jest.spyOn(info, 'information')
const data = {'name':'name'}
//this is already resolving the value, no need for the promise
spy.mockResolvedValue(data);
const {getByTestId, debug} = render(<Logo />);
//You better wait for the spy being called first and then checking
expect(spy).toHaveBeenCalled();
//Spy function involves a state update, wait for it to be updated
await waitForElement(() => getByTestId("logo-text"));
expect(getByTestId("logo-text").props.children).toEqual(data.name);
});
Also, you should move your information call inside a componentDidMount

Issue accessing store and calling actionCreators

I'm in the process of learning React Native with Redux and can't wrap my head around how to access the store/actionCreators from within my components. I feel like I've tried ten or so variants of code, watched most of Dan Abramov's Egghead videos (multiple times), and still don't understand what I'm doing wrong. I think the best explanation I've seen so far is the accepted answer here: Redux, Do I have to import store in all my containers if I want to have access to the data?
The error I get is: undefined is not an object (evaluating 'state.clinic.drName'). Here are the relevant bits of code:
I'm passing the store via the Provider (so I think):
index.ios.js
import clinicReducer from './reducers/clinic';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
const rootReducer = combineReducers({
clinic : clinicReducer
});
let store = createStore(combineReducers({clinicReducer}));
//testing - log every action out
let unsubscribe = store.subscribe( () =>
console.log(store.getState())
);
class ReactShoeApp extends React.Component {
render() {
return (
<Provider store={store}>
<ReactNative.NavigatorIOS
style={styles.container}
initialRoute={{
title: 'React Shoe App',
component: Index
}}/>
</Provider>
);
}
}
Here is my actionCreator:
export const CLINIC_DR_NAME_UPDATE = 'CLINIC_DR_NAME_UPDATE_UPDATE';
export function updateClinicDrName(newValue) {
return {type: CLINIC_DR_NAME_UPDATE, value: newValue};
}
Here is my reducer:
import {
CLINIC_DR_NAME_UPDATE
} from '../actions/';
let cloneObject = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
const initialState = {
drName : null
};
export default function clinicReducer(state = initialState, action) {
switch (action.type) {
case CLINIC_DR_NAME_UPDATE:
return {
...state,
drName: action.value
};
default:
return state || newState;
}
}
and here is my component:
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
const mapStateToProps = function(state){
return {
drName: state.clinic.drName,
}
};
const mapDispatchToProps = function (dispatch) {
return bindActionCreators({
updateClinicDrName: Actions.updateClinicDrName,
}, dispatch)
};
//class definition
class Clinic extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<View style={styles.rowContainer}>
<View style={styles.row}>
<TextGroup label={'Dr. Name'} placeholder={'placeholder'} value={this.props.drName} onChangeText={(text) => this.handlers.onNameChange({text})}></TextGroup>
<TextGroup label={'City'} placeholder={'placeholder'}></TextGroup>
</View>
</View>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Clinic);
All I'm trying to do is update the store (clinic.drName) when the user types in the Dr Name Text Group. Any and all help is appreciated.
The issue is you're mismatching the way you call combineReducers() with the name of the slice you're expecting. This is a common mistake. You're defining an object with the key clinicReducer, but expecting the data to be at a key named clinic.
See Defining State Shape for an explanation of the problem.
Also, your clinicReducer() function looks odd, and has several issues:
Don't access variables outside the function like you are with newState.
Don't do deep-cloning like that (per Redux FAQ: Performance). Instead, use "shallow cloning" to update the state if necessary (see examples at Immutable Update Patterns).
It looks like the reducer is double-nesting the data it's trying to access as well.
I think you want something more like:
// clinicReducer.js
const initialState = {
drName : null
};
export default function clinicReducer(state = initialState, action) {
switch(action.type) {
case CLINIC_DR_NAME_UPDATE: {
return {
...state,
drName : actino.value
};
}
default: return state
}
}
// index.js
import clinicReducer from "./reducers/clinic";
const rootReducer = combineReducers({
clinic : clinicReducer
});
What errors are you getting? I see two issues with what you have.
newState.clinic.drName = action.value; should be throwing a typeError. So for this to work you need to change newState to:
const newState = {
clinic: {}
}
What is this.handlers.onNameChange({text})? I think you need to change this to this.props.updateClinicDrName(text).