How to avoid duplication of components using React-Navigation? - javascript

I had 5 screens with the same styles and layout, but some text and button logic difference. I tried to keep everything in one component, passing there the name of the component I need to mimic, but it grew up in nested if/else all around, what made the code very intricate. What is lesser of these two evils and what should I do: duplicate the components in favor of simplicity or keep them in one place and lose the readability?
Here's the "all in one" component
const Pin = props => {
const {
navigation: { navigate, getParam },
loading,
savePin,
signIn,
toggleModal,
} = props
const [pin, setPin] = useState('')
const isInSignInStack = getParam('isInSignInStack') ? 'isInSignInStack' : false
const isConfirmPinSignUp = getParam('isConfirmPinSignUp') ? 'isConfirmPinSignUp' : false
const isChangePin = getParam('isChangePin') ? 'isChangePin' : false
const isEnterNewPin = getParam('isEnterNewPin') ? 'isEnterNewPin' : false
const isConfirmNewPin = getParam('isConfirmNewPin') ? 'isConfirmNewPin' : false
const newPin = getParam('newPin')
const handleSavePin = () => savePin(pin).then(() => navigate('ConfirmPinSignUp'))
const navigateHome = () => navigate('Home')
const handleAuthenticate = () =>
compose(
then(navigateHome),
then(signIn),
savePin
)(pin)
const validatePin = () =>
isConfirmNewPin
? equals(newPin, pin)
? savePin(pin).then(() => navigate('SuccessPinChange'))
: toggleModal('pin isn't match')
: getPin().then(({ password }) =>
equals(password, pin)
? navigate(isChangePin ? 'ConfirmNewPin' : 'Success', { ...(isChangePin ? { newPin: pin } : {}) })
: toggleModal('pin isn't match')
)
const textObj = {
isInSignInStack: 'Enter your pin',
isConfirmPinSignUp: 'Enter your pin once again',
isChangePin: 'Enter your old pin',
isEnterNewPin: 'Enter the new pin',
isConfirmNewPin: 'Enter the new pin once again',
}
return (
<Container style={styles.container}>
<Content scrollEnabled={false} contentContainerStyle={styles.content}>
<Text style={styles.headerText}>
{pathOr(
'Come up with the new pin',
isInSignInStack || isConfirmPinSignUp || isChangePin || isEnterNewPin || isConfirmNewPin,
textObj
)}
</Text>
<View style={styles.inputView}>
<CodeInput />
</View>
{isConfirmPinSignUp || (
<View style={styles.aknowledgementView}>
{isInSignInStack
? <Text style={styles.text} onPress={handleForgotPassword}>
FORGOT PIN
</Text>
: isEnterNewPin && (
<>
<Text style={styles.greenText}>Attention! Don't use your old pin</Text>
<Text style={styles.greenText}>codes or passwords, come up with the new one</Text>
</>
)}
</View>
)}
<Button
style={isEmpty(pin) ? styles.btnDisabled : styles.btn}
onPress={() =>
isInSignInStack
? handleAuthenticate
: anyTrue(isConfirmPinSignUp, isChangePin) ? validatePin : handleSavePin
}
disabled={anyTrue(isEmpty(pin), loading)}
>
{loading ? <Spinner color="black" /> : <Text style={styles.btnText}>Next</Text>}
</Button>
</Content>
</Container>
)
}
Pin.navigationOptions = ({ navigation: { getParam } }) => {
const isInSignInStack = getParam('isInSignInStack')
const isChangePin = getParam('isChangePin')
const isEnterNewPin = getParam('isEnterNewPin')
return {
title: isInSignInStack ? 'SignIn' : anyTrue(isChangePin, isEnterNewPin) ? 'Change PIN' : 'Register'
}
}
const styles = StyleSheet.create({
//
})
Pin.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
getParam: PropTypes.func,
}).isRequired,
loading: PropTypes.bool.isRequired,
savePin: PropTypes.func.isRequired,
toggleModal: PropTypes.func.isRequired,
signIn: PropTypes.func.isRequired,
}
const mapStateToProps = compose(
pick(['loading']),
path(['user'])
)
const mapDispatchToProps = {
savePin,
signIn,
toggleModal,
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Pin)

Make a generic button component that takes a click handler and text as a prop then simply pass the values as props:
for instance if you have some button component like:
export const Button = ({ children, handler }) =>
<button onPress={handler}>
{children}
</button>;
Then you could use it like
<Button handler={this.yourClickHandler} >{"Some Text"}</Button>

The answer is that you should not couple the components which belong to different use cases as they will change for a different reasons and at different times, even though they look identically now they will change later. Don't write the "super component" that cover all the use cases, because it will become a mess very quickly

Related

React Native, how to make redux state false again

I created a view cart in which I show total price and view cart button, when I add item it makes condition true and display that cart below in every screen, but when I click view cart it's not making it false again, how can I do this? can someone check my code and tell me please. Below is my code
Viewcart.js
<View>
{this.props.show && this.props.items.length > 0 ? (
<View style={styles.total}>
<Text style={styles.totaltext}>Total:</Text>
<Text style={styles.priceTotal}>{this.props.total}</Text>
<View style={styles.onPress}>
<Text
style={styles.pressText}
onPress={() => {
RootNavigation.navigate("Cart");
this.props.show;
}}
>
View Cart
</Text>
</View>
</View>
) : null}
</View>
const mapStateToProps = (state) => {
return {
show: state.clothes.show,
};
};
const mapDispatchToProps = (dispatch) => {
return {
showCart: () => dispatch(showCart()),
};
};
reducer.js
if (action.type === SHOW_CART) {
let addedItem = state.addedItems;
if (addedItem.length === 0) {
return {
...state,
show: state.showCart,
};
} else {
return {
...state,
show: action.showCart,
};
}
}
const initialstate = {
showCart: false
}
action.js
export const showCart = (id) => {
return {
type: SHOW_CART,
showCart: true,
id,
};
};
As per the chat the requirement is to toggle this when exiting the screen so the easiest way to do that is to use the lifecycle methods.
To hide use componentDidMount
componentDidMount(){
this.props.showCartOff();
}
to show use component
componentWillUnmount(){
this.props.showCart();
}

Why isnt running the function from class component in another component? REACT-NATIVE

I have a stacknavigator and in headerTitle have a header component for each screen, heres the code:
const Home_stack = createStackNavigator({ //hooks
Home: {
screen: Home,
navigationOptions: ({navigation}) => {
return {
headerTitle: () => <Header navigation = {navigation} title = "Shum Note"/>}
}
},
Create: {
screen: Create,
navigationOptions: ({navigation}) => {
return {
headerTitle: () => <Childs_header navigation = {navigation} title = "Create Note"/>}
}
},
Edit: {
screen: Edit,
navigationOptions: ({navigation}) => {
return {
headerTitle: () => <Childs_header navigation = {navigation} title = "Edit Note"/>}
}
},
});
and this is the component Childs_header:
import Create_note from "../components/Create_note";
class Header extends Component {
comun = new Create_note();
render() {
return (
<>
<View style ={{backgroundColor: "white", flexDirection: "row", alignItems: "center"}}>
<View>
<Text style = {{color: "black", fontSize: 30, marginLeft: -20}}>{this.props.title}
</Text>
</View>
<View>
<Text>
<Feather name="check-square" size={24} color="black" onPress = {() => this.comun.save_data(this.props.navigation)}/>
</Text>
</View>
</View>
</>
);
}
}
export default Header;
as you can see I import the component Create_note and create an object of it to use one of its function, in this case save_data, but for some reason it isnt working, dont know if it has something to do with AsyncStorage becase with console.log("hi") it works, but saving data it doesnt, heres the structure of create_note component:
class Create_note extends Component {
state = {
content: "",
default_color: "#87cefa", //default color (cyan)
}
save_data = async() => {
if (this.state.content === "") {
//navigation.navigate("Home");
}else {
let clear_content = this.state.content.replace(/ /g,""); //replace al
try {
const data = await AsyncStorage.getItem("data");
if (data === null) {
const data = {"array_notes": [], "last_note": 0};
const last_note = data.last_note + 1;
const new_note = {note_number: last_note, content: clear_content, color: this.state.default_color, text_color: this.state.color}; //create a new_note object, note_number will be the key for each note
const array_notes = [];
array_notes.push(new_note);
data.array_notes = array_notes;
data.last_note = last_note;
await AsyncStorage.setItem("data", JSON.stringify(data)); //using stringify to save the array
//navigation.navigate("Home");
}else {
const data = JSON.parse(await AsyncStorage.getItem("data")); //use parse to acces to the data of the array
const last_note = data.last_note + 1;
const new_note = {note_number: last_note, content: clear_content, color: this.state.default_color, text_color: this.state.color};
const array_notes = data.array_notes;
array_notes.push(new_note);
data.array_notes = array_notes;
data.last_note = last_note;
await AsyncStorage.setItem("data", JSON.stringify(data));
//navigation.navigate("Home");
}
}catch(error) {
alert(error);
}
}
}
render() {
const props = {
screen: "create_note",
change_color: this.change_color.bind(this),
update_color: this.update_color.bind(this),
}
return (
<>
<ScrollView>
<RichEditor
ref = {this.richText}
onChange = {text => this.setState({content: text}, () => console.log(this.state.content))}
allowFileAccess = {true}>
</RichEditor>
</ScrollView>
{this.state.change_color ?
<Color
{...props}>
</Color>
: null}
<RichToolbar
editor = {this.richText}
onPressAddImage = {this.insertImage}
actions = {[
actions.insertBulletsList,
actions.insertOrderedList,
actions.insertImage,
"change_text_color",
]}
iconMap ={{
[actions.insertBulletsList]: () => <Text style = {this.styles.icon}><MaterialIcons name = "format-list-bulleted" size = {this.option_icon.size} color = {this.option_icon.color}/></Text>,
[actions.insertOrderedList]: () => <Text style = {this.styles.icon}><MaterialIcons name = "format-list-numbered" size = {this.option_icon.size} color = {this.option_icon.color}/></Text>,
[actions.insertImage]: () => <Text style = {this.styles.icon}><MaterialIcons name = "image" size = {this.option_icon.size} color = {this.option_icon.color}/></Text>,
change_text_color: () => <Text style = {this.styles.icon}><MaterialIcons name = "format-color-text" size = {this.option_icon.size} color = {this.option_icon.color}/></Text>,
}}
change_text_color = {this.change_color}
style = {{backgroundColor: "white"}}>
</RichToolbar>
<Button title = "save" onPress = {this.save_data}></Button>
</>
);
}
heres an image so you can see better the structure:
the function should run when I click in the check icon, in the blue button works because its part of the create_note component, but I want it in the check icon
From looking at your code I think the problem is that you're passing the navigation state object as a parameter to your save_data function in the onClick of your checkmark.
this.comun.save_data(this.props.navigation)
but the function definition of save_data doesn't take any parameters:
save_data = async () => {
// ...
};
So you could change the save_data function to something like this
save_data = async (navigation) => {
// ...
};
in order to have it work from inside the Header component.
If you want the save button, rendered by the Create_note component, to also call save_data onPress; you will have to pass the navigation state there as well.

React Native - Dynamic State? Or equivalent solution

I am trying to build a view which displays alarm codes - these are delivered to the app in a data array as follows:
alarm:[{ location: "Main Door", code:"123456"}, { location: "Back Door", code:"456789"}],
For each instance there could be 1 or many codes.
I am displaying the codes via this map function:
return this.state.alarmsOnSite.map((data, index) => {
return (
<View key={index}>
<Text style={GlobalStyles.SubHeading}>
Alarm: {data.location}
</Text>
<View style={[GlobalStyles.GreyBox, {position:'relative'}]}>
<Text style={GlobalStyles.starText}>
********
</Text>
<TouchableOpacity
style={CheckInStyles.eyeballImagePlacement}
>
<View style={CheckInStyles.eyeballImage} >
<Image
style={CheckInStyles.eyeballImageImage}
source={require('../images/icons/ico-eyeball.png')}
/>
</View>
</TouchableOpacity>
</View>
</View>
)
});
The brief states that on press of the touchable opacity - the stars should switch to display the code for 5 seconds only. I was thinking this would be easy with state - I could switch a display class on two Text objects to hide/show stars or code. But how do I do this with fixed state if I don't know how many alarm codes there will be? Can I use a dynamic state - is there such a thing - or does anyone have any other ideas for best approach in this situation please?
When setting up your state, include a property in the objects for whether they're showing:
this.state = {
alarmsOnSite: whereverYoureGettingTheDataNow.map(obj => ({...obj, showing: false})),
// ...
};
Then in response to a touch, set that flag to true and then back to false after five seconds. For instance, if the touch is on the ToucableOpacity itself (sorry, I don't know that component):
<View style={[GlobalStyles.GreyBox, {position:'relative'}]}>
<Text style={GlobalStyles.starText}>
{data.showing ? data.code : "********"}
</Text>
<TouchableOpacity
style={CheckInStyles.eyeballImagePlacement}
onTouch={() => this.showAlarm(data)}
>
<View style={CheckInStyles.eyeballImage} >
<Image
style={CheckInStyles.eyeballImageImage}
source={require('../images/icons/ico-eyeball.png')}
/>
</View>
</TouchableOpacity>
</View>
...where showAlarm is:
showAlarm(alarm) {
let updated = null;
this.setState(
({alarmsOnSite}) => ({
alarmsOnSite: alarmsOnSite.map(a => {
if (a === alarm) {
return updated = {...a, showing: true};
}
return a;
})
}),
() => {
setTimeout(() => {
this.setState(({alarmsOnSite}) => ({
alarmsOnSite: alarmsOnSite.map(a => a === updated ? {...a, showing: false} : a)
}));
}, 5000);
}
);
}
...or similar.
Here's a simplified example:
const whereverYoureGettingTheDataNow = [{ location: "Main Door", code:"123456"}, { location: "Back Door", code:"456789"}];
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
alarmsOnSite: whereverYoureGettingTheDataNow.map(obj => ({...obj, showing: false})),
// ...
};
}
showAlarm(alarm) {
let updated = null;
this.setState(
({alarmsOnSite}) => ({
alarmsOnSite: alarmsOnSite.map(a => {
if (a === alarm) {
return updated = {...a, showing: true};
}
return a;
})
}),
() => {
setTimeout(() => {
this.setState(({alarmsOnSite}) => ({
alarmsOnSite: alarmsOnSite.map(a => a === updated ? {...a, showing: false} : a)
}));
}, 5000);
}
);
}
render() {
return <div>
{this.state.alarmsOnSite.map((data, index) => (
<div key={index}>
{data.location}
<div onClick={() => this.showAlarm(data)}>
{data.showing ? data.code : "********"}
</div>
</div>
))}
</div>;
}
}
ReactDOM.render(<Example/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.development.js"></script>

Enzyme button simulate doesn't work as expected

I have the following react-native test code.
import { shallow } from 'enzyme';
import React from 'react';
import {
BorderlessButton,
InputBox,
ProgressBar,
} from 'components';
import Name from '../name.component';
describe('Name component', () => {
let wrapper: any;
const mockOnPress = jest.fn();
const mockSaveStep = jest.fn();
const mockProps: any = {
errors: null,
values: [{ givenName: 'givenName', familyName: 'familyName' }],
};
beforeEach(() => {
wrapper = shallow(<Name signUpForm={mockProps} saveStep={mockSaveStep} />);
});
it('should render Name component', () => {
expect(wrapper).toMatchSnapshot();
});
it('should render 2 <InputBox />', () => {
expect(wrapper.find(InputBox)).toHaveLength(2);
});
it('should render a <ProgressBar />', () => {
expect(wrapper.find(ProgressBar)).toHaveLength(1);
});
it('should render a <BorderlessButton /> with the text NEXT', () => {
expect(wrapper.find(BorderlessButton)).toHaveLength(1);
expect(wrapper.find(BorderlessButton).props().text).toEqual('NEXT');
});
it('should press the NEXT button', () => {
wrapper.find(BorderlessButton).simulate('click');
expect(mockOnPress).toHaveBeenCalled();
});
});
But the last test doesn't work properly. How can I simulate a this button click? This gives me an error saying
expect(jest.fn()).toHaveBeenCalled().
Expected mock function to have been called, but it was not called.
This is the component.
class NameComponent extends Component {
componentDidMount() {
const { saveStep } = this.props;
saveStep(1, 'Name');
}
disableButton = () => {
const {
signUpForm: {
errors, values,
},
} = this.props;
if (errors && values && errors.givenName && errors.familyName) {
if (errors.givenName.length > 0 || values.givenName === '') return true;
if (errors.familyName.length > 0 || values.familyName === '') return true;
}
}
handleNext = () => {
navigationService.navigate('PreferredName');
}
resetForm = () => {
const { resetForm } = this.props;
resetForm(SIGN_UP_FORM);
navigationService.navigate('LoginMain');
}
render() {
const { name, required } = ValidationTypes;
const { step } = this.props;
return (
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : null}
enabled>
<ScreenContainer
navType={ScreenContainer.Types.LEVEL_THREE}
levelThreeOnPress={this.resetForm}>
<View style={styles.container}>
<View style={{ flex: 1 }}>
<SinglifeText
type={SinglifeText.Types.H1}
label='Let’s start with your legal name'
style={styles.textLabel}
/>
<View style={styles.names}>
<InputBox
name='givenName'
form={SIGN_UP_FORM}
maxLength={22}
placeholder='Given name'
containerStyle={styles.givenNameContainer}
inputContainerStyle={styles.inputContainer}
errorStyles={styles.inputError}
keyboardType={KeyBoardTypes.default}
validations={[required, name]}
/>
<InputBox
name='familyName'
form={SIGN_UP_FORM}
maxLength={22}
placeholder='Family name'
inputContainerStyle={styles.inputContainer}
errorStyles={styles.inputError}
keyboardType={KeyBoardTypes.default}
validations={[required, name]}
/>
</View>
<SinglifeText
type={SinglifeText.Types.HINT}
label='Please use the same name you use with your bank'
style={styles.hint}
/>
</View>
</View>
</ScreenContainer>
<ProgressBar presentage={(step / MANUAL_SIGNUP_STEP_COUNT) * 100} />
<View style={styles.bottomButtonContainer}>
<BorderlessButton
text='NEXT'
disabled={this.disableButton()}
onPress={this.handleNext}
/>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}
How can I solve this??
You create the function mockOnPress(), but mockOnPress() is never injected into the component.
In the component you wrote, NameComponent has a child BorderlessButton component, in which the line, onPress={this.handleNext} is hard-coded in. handleNext() is defined elsewhere as:
handleNext = () => {
navigationService.navigate('PreferredName');
}
To test that the functionality of the button is working, I see two viable options. One is to use dependency injection. Instead of hard-coding the button to call navigationService.navigate('PreferredName'), you could have it execute code that is passed in as a prop. See the following as an example:
it('Button should handle simulated click', function (done) {
wrappedButton = mount(<MyButton onClick={() => done()}>Click me!</BaseButton>)
wrappedButton.find('button').simulate('click')
}
Note that you could take the principle provided in the above example and expand it to your example by passing in the functionality you want to occur onClick as a prop to your NameComponent.
Another option you have, is to test whether clicking the button causes the side effects you want to occur. As written, pressing the button should call, navigationService.navigate('PreferredName'). Is this the intended effect? If so, you can change your test to validate whether navigationService.navigate('PreferredName') was called somehow.

Component not updating in react native redux app

I have two components who use the same reducer and share the same state.
The first one is a form, the second one is a separate component to
update a specific field of that form using
react-native-autocomplete-select.
In the second component, everything works fine. But when I get back
to the first component (the form), the prop that I'm updating in the
second component is now undefined. Only when I leave the component
and come back to it or reload my app does the component display the
correct value.
I'm new to redux and I thought I had figured it out but apparently, I'm still missing something.
I'll try to share as much code as possible in order to make it easy for anyone to help me out but let me know if you want me to share additional code.
I would really like to understand what's going on.
First Component
class EditElem extends Component {
componentWillMount() {
this.props.xFetch();
}
onButtonPress() {
const { name, description, elem_id } = this.props;
this.props.xSave({ name, description, elem_id });
}
render() {
return (
<ScrollView>
<View>
<Text>Information</Text>
<CardSection>
<Input
label="Name"
placeholder="..."
value={this.props.name}
onChangeText={value => this.props.xUpdate({ prop: 'name', value })}
/>
<Text style={styles.labelStyle}>Description</Text>
<Input
placeholder="Write here..."
value={this.props.description}
onChangeText={value => this.props.xUpdate({ prop: 'description', value })}
multiline = {true}
numberOfLines = {4}
/>
<TouchableWithoutFeedback onPress={ () => Actions.selectElem() }>
<View style={styles.wrapperStyle}>
<View style={styles.containerStyle}>
<Text style={styles.labelStyle}>Elem</Text>
<Text adjustsFontSizeToFit style={styles.inputStyle}>{checkElem(this.props.elem_id ? this.props.elem_id.toString() : "0")}</Text>
</View>
</View>
</TouchableWithoutFeedback>
</CardSection>
<Button title="Save Changes" onPress={this.onButtonPress.bind(this)} />
</View>
</ScrollView>
);
}
}
const mapStateToProps = (state) => {
const { name, description, elem_id } = state.x.x;
return { name, description, elem_id };
};
export default connect(mapStateToProps, { xUpdate, xFetch, xSave })(EditElem);
Second Component
class SelectElem extends Component {
componentWillMount() {
this.props.xFetch();
}
saveElem(suggestion) {
let elem_id = suggestion.id;
let text = suggestion.text
this.props.xUpdate({ prop: 'elem', text })
this.props.xUpdate({ prop: 'elem_id', elem_id })
this.props.xSave({ elem_id });
}
render() {
const suggestions = data
const onSelect = (suggestion) => {
this.saveElem(suggestion);
}
return(
<View style={{flex: 1}}>
<AutoComplete
placeholder={checkElem(this.props.elem_id ? this.props.elem_id.toString() : "0")}
onSelect={onSelect}
suggestions={suggestions}
suggestionObjectTextProperty='text'
value={this.props.elem}
onChangeText={value => this.props.xUpdate({ prop: 'elem', value })}
minimumSimilarityScore={0.4}
/>
</View>
)
}
}
const mapStateToProps = (state) => {
const { elem_id, description, name, elem } = state.x.x;
return { elem_id, description, name, elem };
};
export default connect(mapStateToProps, { xUpdate, xFetch, xSave })(SelectElem);
store
const store = createStore(reducers, {}, compose(applyMiddleware(ReduxThunk)));
reducer
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_X:
return { ...state, x: { ...state.x, name: action.payload.name, description: action.payload.description, elem_id: action.payload.elem_id } };
case UPDATE_X:
return { ...state, x: { ...state.x, [action.payload.prop]: action.payload.value }};
case SAVE_X:
return state;
default:
return state;
}
}
Actions
export const xUpdate = ({ prop, value }) => {
return {
type: UPDATE_X,
payload: { prop, value }
};
};
export const xSave = ({ name, description, elem_id }) => {
return (dispatch) => {
axios({
method: 'post',
url: 'https:xxxxxxxxxxxxxxxxxxxx',
data: {_____________________ }
}
}).then(response => {
dispatch({ type: SAVE_X });
}).catch(error => console.log(error))
};
};
Can you check if UPDATE_X, SAVE_X ... are defined? Do you have the right import statement at the top of the file?
Ok so my problem came from my reducer actualy in my SAVE_X:
I had:
case SAVE_X:
return { state };
Instead of this:
case SAVE_X:
return { ...state, elem: { ...state.elem, elem_id: action.payload.elem_id } };

Categories