How To Handle conditional Routing or Component Navigation Without React Router - reactjs

I need to navigate between components based on several conditions, and I do not want routes to be displayed in the browser, say localhost:3000/step1 or localhost:3000/step2. The whole application is guided so that a user have to answer all the steps to reach the final result.
ATM I have a main container which handles the component rendering based on the Redux store value.
import React, { Component } from "react";
class Home extends Component {
renderComponent = screen => {
switch (screen) {
case 'SCREEN_A':
return <ScreenA />;
case 'SCREEN_B':
return <ScreenB />;
case 'SCREEN_C':
return <ScreenC />;
case 'SCREEN_D':
return <ScreenD />;
default:
return <ScreenA />;
}
};
render() {
return <div>{this.renderComponent(this.props.currentScreen)}</div>;
}
}
function mapStateToProps(storeData) {
return {
store: storeData,
currentScreen: storeData.appState.currentScreen,
userData: storeData.userData
};
}
export default connect(mapStateToProps)(Home);
The problem is I have to use dispatch to trigger navigation
navigateTo(screens[destination])
navigateBackward(currentScreen)
navigateForward(currentScreen)
in almost all components. I do have a predefined JSON for each component which contains the destination for each screen.
screens : {
SCREEN_A:{
id: 1,
name: 'SCREEN_A',
next: 'SCREEN_B',
back: 'WELCOME_SCREEN',
activeLoader: true,
},
SCREEN_B:{
id: 2,
name: 'SCREEN_B',
next: 'SCREEN_C',
back: 'WELCOME_SCREEN',
activeLoader: true,
},
SCREEN_C:{
id: 3,
name: 'SCREEN_C',
next: 'SCREEN_D',
back: 'SCREEN_A',
activeLoader: true,
},
SCREEN_D:{
id: 4,
name: 'SCREEN_D',
next: 'SCREEN_E',
back: 'SCREEN_D',
activeLoader: true,
},
}
And there are protected screens which makes things way more complicated. Is there a better way of doing this with redux? or should I create a middleware and intercept each state change and calculate the next screen.

I would change a few things:
Make your steps/screens dynamic. By putting them into an Array and using the index to determine the current step it removes a lot of code and will make it easier to add/move steps.
Store the steps/screens config in the redux store.
Optionally, you can pass the nextStep and previousStep to the StepComponent. e.g. <StepComponent nextStep={nextStep} previousStep={previousStep} />.
In your last step, you probably want to call a different action instead of nextStep.
Here's what my solution would look like:
// Home.jsx
import React, { Component } from 'react';
import * as types from '../../redux/Actor/Actor.types';
class Home extends Component {
stepComponents = [
ScreenA,
ScreenB,
ScreenC,
ScreenD,
];
render() {
const { step, steps } = this.props;
const StepComponent = this.stepComponents[step];
return (
<div>
<StepComponent {...steps[step]} />
</div>
);
}
}
// store.jsx
export default {
step : 0,
steps: [
{
id : 1,
name : 'SCREEN_A',
activeLoader: true,
},
....
],
};
// actions.jsx
export const nextStep = () => ({ type: 'NEXT_STEP' });
export const previousStep = () => ({ type: 'PREVIOUS_STEP' });
// reducers.jsx
export const nextStep = state => ({ ...state, step: state.step + 1 });
export const previousStep = state => ({ ...state, step: state.step - 1 });

Related

React-component is not re-rendered when the store is changed, neither automatically nor even by force update

This functional component should display a sorted list with checkboxes at each item that change the values in the store.
For some reason it is not re-rendered when the store is changed. And without a re-renderer, it (and the whole application) works very crookedly and halfway. I suspect that this is because the store object remains the same, albeit with new content. But I don’t understand how to fix it. I have even inserted a force update to the checkbox handler, but for some reason it does not work too.
Component:
import React, { useState, useReducer } from 'react';
import { ReactSortable } from 'react-sortablejs';
import ListItem from '#mui/material/ListItem';
import Checkbox from '#mui/material/Checkbox';
import { connect } from 'react-redux';
import { setGameVisible, setGameInvisible } from '../store/actions/games';
interface IGamesListProps {
games: [];
setGameVisible: (id: string) => void;
setGameInvisible: (id: string) => void;
}
interface ItemType {
id: string;
name: string;
isVisible: boolean;
}
const GamesList: React.FunctionComponent<IGamesListProps> = ({games, setGameVisible, setGameInvisible}) => {
const [state, setState] = useState<ItemType[]>(games);
// eslint-disable-next-line
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); // this way of force updating is taken from the official React documentation (but even it doesn't work!)
const onCheckboxChangeHandle = (id: string, isVisible: boolean) => {
isVisible ? setGameInvisible(id) : setGameVisible(id);
forceUpdate(); // doesn't work :(((
}
return (
<ReactSortable list={state} setList={setState} tag='ul'>
{state.map((item) => (
<ListItem
sx={{ maxWidth: '300px' }}
key={item.id}
secondaryAction={
<Checkbox
edge="end"
onChange={() => onCheckboxChangeHandle(item.id, item.isVisible)}
checked={item.isVisible}
/>
}
>
{item.name}
</ListItem>
))}
</ReactSortable>
);
};
export default connect(null, { setGameVisible, setGameInvisible })(GamesList);
Reducer:
import { SET_GAMES, SET_GAME_VISIBLE, SET_GAME_INVISIBLE } from '../actions/games';
export const initialState = {
games: [],
};
export default function games(state = initialState, action) {
switch(action.type) {
case SET_GAMES: {
for(let obj of action.payload.games) {
obj.isVisible = true;
}
return {
...state,
games: action.payload.games,
};
}
case SET_GAME_VISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = true;
};
}
return {
...state,
};
}
case SET_GAME_INVISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = false;
};
}
return {
...state,
};
}
default:
return state;
}
}
Thank you for any help!
Note: By the information You gave I came with the idea of the problem, but I posted here because it is going to be explanatory and long.
First of all, you don't pass the new game via mapStateToProps into Component in a state change, and even you do, useState won't use new game prop value for non-first render. You must use useEffect and trigger changes of the game and set the to state locally.
At this point you must find the inner state redundant and you can remove it and totally rely on the redux state.
const mapStateToProp = (state) => ({
games: state.games // you may need to change the path
})
connect(mapStateToProp, { setGameVisible, setGameInvisible })(GamesList);
Second, the reducer you made, changes the individual game item but not the games list itself. because it is nested and the reference check by default is done as strict equality reference check-in redux state === state. This probably doesn't cause an issue because the outer state changes by the way, but I think it worth it to mention it.
for(let obj of action.payload.games) {
obj.isVisible = true; // mutating actions.payload.games[<item>]
}
return {
...state,
games: [...action.payload.games], // add immutability for re-redenr
};
// or use map
return {
...state,
games: action.payload.games.map(obj => ({...obj, isVisible:true})),
};
Third, It's true your forceUpdate will cause the component to re-render, and you can test that by adding a console.log, but it won't repaint the whole subtree of your component including inner children if their props don't change that's because of performance issue. React try to update as efficiently as possible. Also you the key optimization layer which prevent change if the order of items and id of them doesn't change

React FC Context & Provider Typescript Value Issues

Trying to implement a global context on an application which seems to require that a value is passed in, the intention is that an API will return a list of organisations to the context that can be used for display and subsequent API calls.
When trying to add the <Provider> to App.tsx the application complains that value hasn't been defined, whereas I'm mocking an API response with useEffect().
Code as follows:
Types types/Organisations.ts
export type IOrganisationContextType = {
organisations: IOrganisationContext[] | undefined;
};
export type IOrganisationContext = {
id: string;
name: string;
};
export type ChildrenProps = {
children: React.ReactNode;
};
Context contexts/OrganisationContext.tsx
export const OrganisationContext = React.createContext<
IOrganisationContextType
>({} as IOrganisationContextType);
export const OrganisationProvider = ({ children }: ChildrenProps) => {
const [organisations, setOrganisations] = React.useState<
IOrganisationContext[]
>([]);
React.useEffect(() => {
setOrganisations([
{ id: "1", name: "google" },
{ id: "2", name: "stackoverflow" }
]);
}, [organisations]);
return (
<OrganisationContext.Provider value={{ organisations }}>
{children}
</OrganisationContext.Provider>
);
};
Usage App.tsx
const { organisations } = React.useContext(OrganisationContext);
return (
<OrganisationContext.Provider>
{organisations.map(organisation => {
return <li key={organisation.id}>{organisation.name}</li>;
})}
</OrganisationContext.Provider>
);
Issue #1:
Property 'value' is missing in type '{ children: Element[]; }' but required in type 'ProviderProps<IOrganisationContextType>'.
Issue #2:
The list is not rendering on App.tsx
Codesandbox: https://codesandbox.io/s/frosty-dream-07wtn?file=/src/App.tsx
There are a few different things that you'll need to look out for in this:
If I'm reading the intention of the code properly, you want to render OrganisationProvider in App.tsx instead of OrganisationContext.Provider. OrganisationProvider is the custom wrapper you have for setting the fake data.
Once this is fixed, you're going to run into an infinite render loop because in the OrganisationProvider component, the useEffect sets the organisations value, and then runs whenever organisations changes. You can probably set this to an empty array value [] so the data is only set once on initial render.
You're trying to use the context before the provider is in the tree above it. You'll need to re-structure it so that the content provider is always above any components trying to consume context. You can also consider using the context consumer component so you don't need to create another component.
With these suggested updates, your App.tsx could look something like the following:
import * as React from "react";
import "./styles.css";
import {
OrganisationContext,
OrganisationProvider
} from "./contexts/OrganisationContext";
export default function App() {
return (
<OrganisationProvider>
<OrganisationContext.Consumer>
{({ organisations }) =>
organisations ? (
organisations.map(organisation => {
return <li key={organisation.id}>{organisation.name}</li>;
})
) : (
<div>loading</div>
)
}
</OrganisationContext.Consumer>
</OrganisationProvider>
);
}
And the updated useEffect in OrganisationsContext.tsx:
React.useEffect(() => {
setOrganisations([
{ id: "1", name: "google" },
{ id: "2", name: "stackoverflow" }
]);
}, []);

How to correctly mock React Navigation's getParam method using Jest

I have a React Native app in which I'm trying to write some integration tests using Jest & Enzyme. My situation is as follows, I have a component which fetches a navigation param being passed to it from the previous screen using getParam - which works fine normally, I'm just struggling to successfully get a value in there using mock data. My code looks like this:
In my container I have this in the render method:
const tickets = navigation.getParam('tickets', null);
Then in my test I have the following:
const createTestProps = (testProps: Object, navProps: any = {}) =>
({
navigation: {
navigate: jest.fn(),
getParam: jest.fn(),
...navProps,
},
...testProps,
} as any);
let props = createTestProps(
{},
{
state: {
// Mock navigation params
params: {
tickets: [
{
cellNumber: '123456789',
ticketId: 'xxx',
},
{
cellNumber: '123456789',
ticketId: 'xxx',
},
],
},
},
}
);
const container = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeProvider theme={theme}>
<TicketSummaryScreen {...props} />
</ThemeProvider>
</MockedProvider>
);
As you can see I've attempted to mock the actual navigation state, which I've checked against what's actually being used in the real component, and it's basically the same. The value for tickets is still undefined each time I run the test. I'm guessing it has to do with how I've mocked the getParam function.
Anyone have any ideas? Would be much appreciated!
I just successfully fixed this problem on my project. The only advantage that I had is that I had the render method being imported from a file I created. This is a great because all my tests can be fixed by just changing this file. I just had to merge some mocked props into the component that render was receiving.
Here's what it looked like before:
/myproject/jest/index.js
export { render } from 'react-native-testing-library'
After the fix:
import React from 'react'
import { render as jestRender } from 'react-native-testing-library'
const navigation = {
navigate: Function.prototype,
setParams: Function.prototype,
dispatch: Function.prototype,
getParam: (param, defaultValue) => {
return defaultValue
},
}
export function render(Component) {
const NewComponent = React.cloneElement(Component, { navigation })
return jestRender(NewComponent)
}
This setup is great! It just saved me many refactoring hours and probably will save me more in the future.
Maybe try returning the mock data from getParam
Try bellow example code.
const parameters = () => {
return "your value"
}
.....
navigation: {
getParam: parameters,
... navProps
},
... testProps
});
Give it a try
const navState = { params: { tickets: 'Ticket1', password: 'test#1234' } }
const navigation = {
getParam: (key, val) => navState?.params[key] ?? val,
}
here navState values will be params that you are passing.

state is not being updated when using React Hooks

I am currently playing around with the new React Hooks feature, and I have run into an issue where the state of a functional component is not being updated even though I believe I am doing everything correctly, another pair of eyes on this test app would be much appreciated.
import React, { useState, useEffect } from 'react';
const TodoList = () => {
let updatedList = useTodoListState({
title: "task3", completed: false
});
const renderList = () => {
return (
<div>
{
updatedList.map((item) => {
<React.Fragment key={item.title}>
<p>{item.title}</p>
</React.Fragment>
})
}
</div>
)
}
return renderList();
}
function useTodoListState(state) {
const [list, updateList] = useState([
{ title: "task1", completed: false },
{ title: "task2", completed: false }
]);
useEffect(() => {
updateList([...list, state]);
})
return list;
}
export default TodoList;
updateList in useTodoListState's useEffect function should be updating the list variable to hold three pieces of data, however, that is not the case.
You have a few problems here:
In renderList you are misusing React.Fragment. It should be used to wrap multiple DOM nodes so that the component only returns a single node. You are wrapping individual paragraph elements each in their own Fragment.
Something needs to be returned on each iteration of a map. This means you need to use the return keyword. (See this question for more about arrow function syntax.)
Your code will update infinitely because useEffect is updating its own hook's state. To remedy this, you need to include an array as a second argument in useEffect that will tell React to only use that effect if the given array changes.
Here's what it should all look like (with some reformatting):
import React, { useState, useEffect } from 'react';
const TodoList = () => {
let updatedList = useTodoListState({
title: "task3", completed: false
});
return (
<React.Fragment> // #1: use React.Fragment to wrap multiple nodes
{
updatedList.map((item) => {
return <p key={item.title}>{item.title}</p> // #2: return keyword inside map with braces
})
}
</React.Fragment>
)
}
function useTodoListState(state) {
const [list, updateList] = useState([
{ title: "task1", completed: false },
{ title: "task2", completed: false }
]);
useEffect(() => {
updateList([...list, state]);
}, [...list]) // #3: include a second argument to limit the effect
return list;
}
export default TodoList;
Edit:
Although I have tested that the above code works, it would ultimately be best to rework the structure to remove updateList from useEffect or implement another variable to control updating.

Remove last route from react navigation stack

So, I have the following screens:
- ChatList
- NewRoom
- ChatRoom
Basically, I don't want to go back to Start a new chat from the just-created chat room ... but instead go directly into the chat rooms list. So far, I came up with the following:
const prevGetStateForActionChatStack = ChatStack.router.getStateForAction
ChatStack.router.getStateForAction = (action, state) => {
if (state && action.type === 'RemovePreviousScreen') {
const routes = state.routes.slice( 0, state.routes.length - 2 ).concat( state.routes.slice( -1 ) )
return {
...state,
routes,
index: routes.length - 1
}
}
return prevGetStateForActionChatStack(action, state)
}
And it theoretically works ... but there is a weird animation when removing the previous route after getting to the new room, as follows. Let me know if you guys have any solution to this issue ...
In react-navigation#3.0
import { StackActions, NavigationActions } from 'react-navigation';
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Profile' })],
});
this.props.navigation.dispatch(resetAction);
https://reactnavigation.org/docs/en/stack-actions.html#reset
In react-navigation#6.0
The reset action is replaced by replace.
import { StackActions } from '#react-navigation/native';
navigation.dispatch(
StackActions.replace('Profile', {user: 'jane',})
);
https://reactnavigation.org/docs/stack-actions/#replace
From your code it seems you are using react-navigation.
React-Navigation has a reset action that allows you to set the screen stack.
For example:
In your case,
Screen 1: Chat room
Screen 2: Chat list
If you want to remove the chatroom screen from your stack you need to write it as
import { NavigationActions } from 'react-navigation'
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'chatlist'})
]
})
this.props.navigation.dispatch(resetAction)
This will reset your stack to only one screen as initial screen that is chatlist.
actions array can have multiple routes and index defines the active route.
For further details refer the following link:
https://reactnavigation.org/docs/navigators/navigation-actions
Resetting the navigation stack for the home screen (React Navigation and React Native)
you should be able to use the following to change the animation:
export const doNotAnimateWhenGoingBack = () => ({
// NOTE https://github.com/react-community/react-navigation/issues/1865 to avoid back animation
screenInterpolator: sceneProps => {
if (Platform.isIos) {
// on ios the animation actually looks good! :D
return CardStackStyleInterpolator.forHorizontal(sceneProps);
}
if (
sceneProps.index === 0 &&
sceneProps.scene.route.routeName !== 'nameOfScreenYouWannaGoTo' &&
sceneProps.scenes.length > 2
)
return null;
return CardStackStyleInterpolator.forVertical(sceneProps);
},
});
and use it as follows:
const Stack = StackNavigator(
{
...screens...
},
{
transitionConfig: doNotAnimateWhenGoingBack,
}
);

Resources