Push to new route without any further actions on the component - javascript

We use an external componet which we don't control that takes in children which can be other components or
used for routing to another page. That component is called Modulation.
This is how we are currently calling that external Modulation component within our MyComponent.
import React, {Fragment} from 'react';
import { withRouter } from "react-router";
import { Modulation, Type } from "external-package";
const MyComponent = ({
router,
Modulation,
Type,
}) => {
// Need to call it this way, it's how we do modulation logics.
// So if there is match on typeA, nothing is done here.
// if there is match on typeB perform the re routing via router push
// match happens externally when we use this Modulation component.
const getModulation = () => {
return (
<Modulation>
<Type type="typeA"/> {/* do nothing */}
<Type type="typeB"> {/* redirect */}
{router.push('some.url.com')}
</Type>
</Modulation>
);
}
React.useEffect(() => {
getModulation();
}, [])
return <Fragment />;
};
export default withRouter(MyComponent);
This MyComponent is then called within MainComponent.
import React, { Fragment } from 'react';
import MyComponent from '../MyComponent';
import OtherComponent1 from '../OtherComponent1';
import OtherComponent2 from '../OtherComponent2';
const MainComponent = ({
// some props
}) => {
return (
<div>
<MyComponent /> {/* this is the above component */}
{/* We should only show/reach these components if router.push() didn't happen above */}
<OtherComponent1 />
<OtherComponent2 />
</div>
);
};
export default MainComponent;
So when we match typeB, we do perform the rerouting correctly.
But is not clean. OtherComponent1 and OtherComponent2 temporarily shows up (about 2 seconds) before it reroutes to new page.
Why? Is there a way to block it, ensure that if we are performing router.push('') we do not show these other components
and just redirect cleanly?
P.S: react-router version is 3.0.0

Related

Finding the buttons on the screen that have no text for the test

I am trying to write the tests for the NavBar component (using react-native-testing-library) that has several buttons that are basically just icons (using ui-kitten for react native). So I can't get these buttons by text (as there is none) but other methods didn't work for me either (like adding accesibilityLabel or testID and then getting by the label text / getting by test ID). Any ideas what I am doing wrong?
// NavBar.tsx
import React from 'react';
import {View, StyleSheet} from 'react-native';
import {HomeBtn, SaveBtn} from '../components/buttons';
import UserSignOut from './UserSignOut';
const NavBar = ({
navigation,
pressHandlers,
}) => {
return (
<View style={styles.navBar}>
<View>
<HomeBtn navigation={navigation} />
<SaveBtn pressHandler={pressHandlers?.saveBtn ?? undefined} />
</View>
<UserSignOut />
</View>
);
};
export default NavBar;
// HomeBtn.tsx
import React from 'react';
import {Button} from '#ui-kitten/components';
import {HomeIcon} from '../shared/icons';
import styles from './Btn.style';
export const HomeBtn = ({navigation}: any) => {
return (
<Button
accesibilityLabel="home button"
style={styles.button}
accessoryLeft={props => HomeIcon(props, styles.icon)}
onPress={() => navigation.navigate('Home')}
/>
);
};
// NavBar.test.tsx
import React from 'react';
import {render, screen} from '#testing-library/react-native';
import * as eva from '#eva-design/eva';
import {RootSiblingParent} from 'react-native-root-siblings';
import {EvaIconsPack} from '#ui-kitten/eva-icons';
import {ApplicationProvider, IconRegistry} from '#ui-kitten/components';
import NavBar from '../../containers/NavBar';
describe('NavBar', () => {
const navBarContainer = (
<RootSiblingParent>
<IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={eva.light}>
<NavBar />
</ApplicationProvider>
</RootSiblingParent>
);
it('should render the buttons', async () => {
render(navBarContainer);
// this test fails (nothing is found with this accesibility label)
await screen.findByLabelText('home button');
});
});
Query predicate
The recommended solution would be to use:
getByRole('button', { name: "home button" })
As it will require both the button role, as well as check accessibilityLabel with name option.
Alternative, but slightly less expressive way would be to use:
getByLabelText('home button')
This query will only check accessibilityLabel prop, which also should work fine.
Why is query not matching
Since you're asking why the query is not working, that depends on your test setup. It seems that you should be able to use sync getBy* query and do not need to await findBy* query, as the HomeBtn should be rendered without waiting for any async action.
What might prevent that test from working could be incorrect mocking of any of the wrapping components: RootSiblingParent, ApplicationProvider, they might be "consuming" children prop without rendering it. In order to diagnose the issue you can use debug() function from RNTL to inspect the current state of rendered components. You can also run your tests on render(<NavBar />) to verify that.
Does await screen.findByA11yLabel('home button') work? It should match the accessibilityLabel prop.

Dynamically import React Components based on input

I want to render different Components based on some checkboxes selection pattern without having to import components that may not be used.
I have an Array which contains the Component names (I used numbers as an example) and I want to import each component based on the values of the array.
I came up with something like this:
import {Suspense} from 'react'
export default function CreationForm() {
const docs = [1,3,5]
return (
<Suspense fallback={<div>Loading...</div>}>
{
docs.map(val => React.lazy(() => import(`documents/${val}.jsx`)))
}
</Suspense>
)
}
I know this solution does not work but I think it explains what I am trying to do.
I could try using state but the actual "docs array" is an state variable in the real application so it could cause duplicated state.
I did this as a test and worked:
const A = React.lazy(() => import(`documents/1.jsx`))
...
*** SNIP ***
...
<Suspense fallback={<div>Loading...</div>}>
{
docs.map((val) => <A/>)
}
</Suspense>
But I cannot dynamically import each component like this.
Ok, so you don't need conditional imports, you just want to do conditional rendering. That's waaaaay simpler.
Example:
import { FormA } from "./FormA";
import { FormB } from "./FormB";
const MyComponent = ({ which }) => {
return <>
{which === "form-a" && <FormA />}
{which === "form-b" && <FormB />}
<>;
};

React - Passing callbacks from React Context consumers to providers

I have the following context
import React, { createContext, useRef } from "react";
const ExampleContext = createContext(null);
export default ExampleContext;
export function ExampleProvider({ children }) {
const myMethod = () => {
};
return (
<ExampleContext.Provider
value={{
myMethod,
}}
>
{children}
<SomeCustomComponent
/* callback={callbackPassedFromConsumer} */
/>
</ExampleContext.Provider>
);
}
As you can see, it renders a custom component which receive a method as prop. This method is defined in a specific screen, which consumes this context.
How can I pass it from the screen to the provider?
This is how I consume the context (with a HOC):
import React from "react";
import ExampleContext from "../../../contexts/ExampleContext";
const withExample = (Component) => (props) =>
(
<ExampleContext.Consumer>
{(example) => (
<Component {...props} example={example} />
)}
</ExampleContext.Consumer>
);
export default withExample;
And this is the screen where I have the method which I need to pass to the context provider
function MyScreen({example}) {
const [data, setData] = useState([]);
const myMethodThatINeedToPass = () => {
...
setData([]);
...
}
return (<View>
...
</View>);
}
export default withExample(MyScreen);
Update:
I am trying to do this because in my real provider I have a BottomSheet component which renders two buttons "Delete" and "Report". This component is reusable, so, in order to avoid repeating myself, I am using a context provider.
See: https://github.com/gorhom/react-native-bottom-sheet/issues/259
Then, as the bottom sheet component which is rendered in the provider can receive optional props "onReportButtonPress" or "onDeleteButtonPress", I need a way to pass the method which manipulates my stateful data inside the screen (the consumer) to the provider.
You can't, in React the data only flows down.
This is commonly called a “top-down” or “unidirectional” data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components “below” them in the tree.
Your callbacks ("onReportButtonPress", "onDeleteButtonPress") must be available at provider's scope.
<ExampleContext.Provider
value={{
onReportButtonPress,
onDeleteButtonPress,
}}
>
{children}
</ExampleContext.Provider>;
Render SomeCustomComponent in Consumer component. This is the React way of doing things :)

Hitting Back button in React app doesn't reload the page

I have a React app (16.8.6) written in TypeScript that uses React Router (5.0.1) and MobX (5.9.4). The navigation works fine and data loads when it should, however, when I click the browser's Back button the URL changes but no state is updated and the page doesn't get re-rendered. I've read endless articles about this issue and about the withRouter fix, which I tried but it doesn't make a difference.
A typical use case is navigating to the summary page, selecting various things which cause new data to load and new history states to get pushed and then going back a couple of steps to where you started. Most of the history pushes occur within the summary component, which handles several routes. I have noticed that when going back from the summary page to the home page the re-rendering happens as it should.
My index.tsx
import { Provider } from 'mobx-react'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
import * as serviceWorker from './serviceWorker'
import * as Utils from './utils/Utils'
const rootStore = Utils.createStores()
ReactDOM.render(
<Provider {...rootStore }>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
)
serviceWorker.unregister()
My app.tsx
import * as React from 'react'
import { inject, observer } from 'mobx-react'
import { Route, Router, Switch } from 'react-router'
import Home from './pages/Home/Home'
import PackageSummary from './pages/PackageSummary/PackageSummary'
import ErrorPage from './pages/ErrorPage/ErrorPage'
import { STORE_ROUTER } from './constants/Constants'
import { RouterStore } from './stores/RouterStore'
#inject(STORE_ROUTER)
#observer
class App extends React.Component {
private routerStore = this.props[STORE_ROUTER] as RouterStore
public render() {
return (
<Router history={this.routerStore.history}>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/summary/:packageId" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName/:appName" component={PackageSummary} />
<Route component={ErrorPage} />
</Switch>
</Router>
)
}
}
export default App
My router store
import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router'
import { createBrowserHistory } from 'history'
export class RouterStore extends BaseRouterStore {
constructor() {
super()
this.history = syncHistoryWithStore(createBrowserHistory(), this)
}
}
How I create the MobX stores
export const createStores = () => {
const routerStore = new RouterStore()
const packageListStore = new PackageListStore()
const packageSummaryStore = new PackageSummaryStore()
const packageUploadStore = new PackageUploadStore()
return {
[STORE_ROUTER]: routerStore,
[STORE_SUPPORT_PACKAGE_LIST]: packageListStore,
[STORE_SUPPORT_PACKAGE_SUMMARY]: packageSummaryStore,
[STORE_SUPPORT_PACKAGE_UPLOAD]: packageUploadStore
}
}
So my questions are:
How can I get the page to load the proper data when the user goes back/forward via the browser?
If the solution is being able to get MobX to observe changes to the location, how would I do that?
You could implement something like this in your component:
import { inject, observer } from 'mobx-react';
import { observe } from 'mobx';
#inject('routerStore')
#observer
class PackageSummary extends React.Component {
listener = null;
componentDidMount() {
this.listener = observe(this.props.routerStore, 'location', ({ oldValue, newValue }) => {
if (!oldValue || oldValue.pathname !== newValue.pathname) {
// your logic
}
}, true)
}
componentWillUnmount() {
this.listener();
}
}
Problem with this approach is that if you go back from /summary to other page (e.g. '/'), callback will initiate, so you would also need some kind of check which route is this. Because of these kind of complications I would suggest using mobx-state-router, which I found much better to use with MobX.
React router monitors url changes and renders associated component defined for the route aka url.
You have to manually refresh or call a window function to reload.
If I remember correctly, using a browser back function does not reload the page (I might be wrong).
Why not try to detect the back action by a browser and reload the page when detected instead?
You can try the following code to manually reload the page when the browser back button is clicked.
$(window).bind("pageshow", function() {
// Run reload code here.
});
Also out of curiosity, why do you need so many different stores?
In App.js
useEffect(() => {
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload();
}
};
}, []);

Console.Log Not Being Called Inside React Constructor

I'm trying to add a component to a default .NET Core MVC with React project. I believe I have everything wired up to mirror the existing "Fetch Data" component, but it doesn't seem like it's actually being called (but the link to the component in my navbar does move to a new page).
The component itself...
import React, { Component } from 'react';
export class TestComponent extends Component {
static displayName = TestComponent.name;
constructor (props) {
super(props);
console.log("WHO NOW?");
this.state = { message: '', loading: true, promise: null };
this.state.promise = fetch('api/SampleData/ManyHotDogs');
console.log(this.state.promise);
}
static renderForecastsTable (message) {
return (
<h1>
Current Message: {message}
</h1>
);
}
render () {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: TestComponent.renderForecastsTable(this.state.message);
return (
<div>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
);
}
}
The App.js
import React, { Component } from 'react';
import { Route } from 'react-router';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { FetchData } from './components/FetchData';
import { Counter } from './components/Counter';
import { TestComponent } from './components/TestComponent';
export default class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={Home} />
<Route path='/counter' component={Counter} />
<Route path='/fetch-data' component={FetchData} />
<Route path='/test-controller' component={TestComponent} />
</Layout>
);
}
}
That console.log("Who now") is never called when I inspect, and the page remains totally blank. I can't find a key difference between this and the functioning components, and google has not been much help either. Any ideas what is missing?
Edit
While troubleshooting this, I ended up creating a dependency nightmare that broke the app. Since I'm only using the app to explore React, I nuked it and started over--and on the second attempt I have not been able to reproduce the not-rendering issue.
It is advisable to use componentDidMount to make the call to the REST API with the fetch or axios.
class TestComponent extends Component{
constructor(props){
state = {promise: ''}
}
async componentDidMount () {
let promise = await fetch ('api / SampleData / ManyHotDogs');
this.setState ({promise});
console.log (promise);
}
render(){
return(
<div>{this.state.promise}</div>
);
}
}

Categories

Resources