Component not rendering when implementing useHistory from react-router-dom - javascript

I am unclear as to why I am getting my URL to redirect but it's not rendering the corresponding component. I am referring to the Result component. I am not getting any errors, it is successfully unmounting OnfidoSDK, but it's not rendering my Result component.
import React, {useEffect, useState} from 'react';
import axios from 'axios';
import * as OnfidoSDK from 'onfido-sdk-ui/dist/onfido.min.js';
import 'onfido-sdk-ui/dist/style.css';
import {useHistory} from 'react-router-dom';
const onfidoContainerId = 'onfido-sdk-wrapper';
const transmitAPI = 'third/party/api/url';
const useOnfidoFetch = (URL) => {
const [token, setToken] = useState();
const [id, setId] = useState();
const history = useHistory();
useEffect(() => {
axios
.get("http://localhost:5000/post_stuff")
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
console.log("this is the json data", json_data);
const id = json_data.applicant_id;
const token = json_data.onfido_sdk_token;
setId(id);
setToken(token);
});
}, [URL]);
useEffect(() => {
if (!token) return;
console.log("this is working!");
onfidoOut = OnfidoSDK.init({
token,
containerId: "root",
steps: [
{
type: "welcome",
options: {
title: "Open your new bank account",
},
},
"document",
],
onComplete: function (data) {
console.log("everything is complete");
console.log("this is the applicant id", id);
let obj;
axios
.post("http://localhost:5000/post_id", {
applicant_id: id,
})
.then((response) => {
obj = response.data.data.data.json_data.result;
onfidoOut.tearDown();
handleRedirect();
});
function handleRedirect() {
history.push('/result');
}
},
});
}, [id, token]);
};
export default function() {
const URL = `${transmitAPI}/anonymous_invoke?aid=onfido_webapp`;
const result = useOnfidoFetch(URL, {});
return (
<div id={onfidoContainerId} />
)
}
I am not getting any errors in console. This is the App.js file with all of my components:
import Landing from './components/Landing';
import Onfido from './components/Onfido';
import Result from './components/Result';
export default () => {
return (
<div>
<StylesProvider>
<BrowserRouter>
<Switch>
<Route exact path="/onfido" component={Onfido} />
<Route exact path="/result" component={Result} />
<Route path="/" component={Landing} />
</Switch>
</BrowserRouter>
</StylesProvider>
</div>
);
};
Now this is a microfrontend application. And in the container app inside of src/components./MarketingApp.js, I have is written in the following manner:
import { mount } from "marketing/MarketingApp";
import React, { useRef, useEffect } from "react";
import { useHistory } from "react-router-dom";
export default () => {
const ref = useRef(null);
const history = useHistory();
useEffect(() => {
const { onParentNavigate } = mount(ref.current, {
initialPath: history.location.pathname,
onNavigate: ({ pathname: nextPathname }) => {
const { pathname } = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
});
history.listen(onParentNavigate);
}, []);
return <div ref={ref} />;
};
Since it's already using useHistory in the container, could that somehow be colliding with using useHistory on the sub app?
When I do a console log of history on the sub app, I get back an object with the property of location, pathname: "/result"
So on troubleshooting, I noticed that if I refresh the page, then the component renders. Not sure why that is though. Or could the issue be because I am rendering it in this manner:
import React from "react";
const onfidoHTML = `<h1>Thank You!</h1><H2>Your documents have been submitted for review.</h2>`;
export default function Result() {
return <div dangerouslySetInnerHTML={{ __html: onfidoHTML }} />;
}
I am properly using history, in my container app, there is a src/bootstrap.js file:
import React from 'react';
import ReactDOM from 'react-dom';
import {createMemoryHistory, createBrowserHistory} from 'history';
import App from './App';
const mount = (el, {onNavigate, defaultHistory, initialPath}) => {
const history = defaultHistory || createMemoryHistory({
initialEntries: [initialPath],
});
if (onNavigate) {
history.listen(onNavigate);
}
ReactDOM.render(<App history={history} />, el);
return {
onParentNavigate({ pathname: nextPathname }) {
const { pathname } = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
};
};
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root');
if (devRoot) {
mount(devRoot, {defaultHistory: createBrowserHistory()});
}
}
export { mount };

Related

How to fix useDispatch REACT hook being called conditionally when mocking dispatch

Error Message: React Hook "useDispatch" is called conditionally. React Hooks must be called in the exact same order in every component render
I've been trying to figure out how to fix this for days, but nothing seeems to work. The component works when I don't mock anything, but as soon as I mock dispatch it gives me this error.
Here's my component:
import { Stage } from "../Stage/Stage";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { retrieveStageList } from "../../modules/reducer";
import { Process } from "../Process/Process";
export function RenderProcess({
_useSelector = useSelector,
_useDispatch = useDispatch(), //this is where it breaks
_Process = Process,
}) {
const dispatch = _useDispatch();
const process = _useSelector((state) => state.renderProcess);
const stageList = _useSelector((state) => state.stageList);
useEffect(() => {
if (process.processId !== null)
dispatch(retrieveStageList(process.processId));
}, []);
return (
<>
<_Process process={process} />
{stageList?.map((stageInputs, processId) => {
return (
<div key={processId}>
<Stage stage={stageInputs} />
</div>
);
})}
</>
);
}
Here's my test for this component:
import { render } from "#testing-library/react";
import { RenderProcess } from "./RenderProcess";
test("should call dispatch once.", () => {
const _useSelector = (fn) =>
fn({
stageList: [],
renderProcess: { processId: "309624b6-9c96-4ba7-8f7e-78831614f685" },
});
const dispatch = jest.fn();
render(
<RenderProcess
_useSelector={_useSelector}
_useDispatch={() => dispatch}
_Process={() => {}}
/>
);
expect(dispatch).toHaveBeenCalledTimes(1);
});
Any help on this would be amazing.
Found the fix, in the properties being passed in RenderProcess- on this
line:
_useDispatch = useDispatch()
it should be:
_useDispatch = useDispatch

useSelector not updating even after dispatch

I'm trying to display products using the fetched axios result from reducer, but the useSelector value just won't change and is still empty even after dispatch. I have checked the axios result and the response has correct data. Does it have something to do with this line on redux documentation?
With useSelector(), returning a new object every time will always force a re-render by default.
reducer
import axios from "axios";
export const products = (state = [], action) => {
switch (action.type) {
case "FETCH_PRODUCTS": {
const uri = "/products";
axios.get(uri).then(function (response) {
if (response.status == 200) {
console.log(response.data.products); // ===> correct new value
return { state: response.data.products };
}
});
}
App.js
import React, { useEffect } from "react";
import { shallowEqual, useSelector, useDispatch } from "react-redux";
import "../css/App.css";
import { Products, Navbar, Cart } from "../components";
function App() {
const dispatch = useDispatch();
const products = useSelector((state) => state.products, shallowEqual);
const cart = useSelector((state) => state.cart, shallowEqual);
useEffect(() => {
dispatch({
type: "FETCH_PRODUCTS",
});
console.log(products); // ===> still empty array
}, []);
return (
<div className="App">
<Navbar />
<Cart cart={cart} />
<Products products={products} />
</div>
);
}
export default App;
You should first create your action
import axios from 'axios';
export const fetchProducts = () => async (dispatch) => {
try {
const { data } = await axios.get('...');
dispatch({ type: "FETCH_PRODUCTS", payload: data.result });
} catch (error) {
console.log(error);
}
};
Then, Use dispatch and action together
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchProducts } from './actions';
const getSelectors = state => ({ cart: state.cart, products: state.products });
const App = () => {
const dispatch = useDispatch();
const {cart, products} = useSelector(getSelectors);
useEffect(() => {
dispatch(fetchProducts());
}, []);
return (
<div className="App">
<Navbar />
<Cart cart={cart} />
<Products products={products} />
</div>
);
};

React Navigation Linking not working with conditional stack rendering

I am making a expo (react-native) project and use React Navigation.
I use conditional stack rendering based on if the user is logged in to my app. I also need to use deep linking to connect to my app and open specific screens. When I run my app with the url from a cold start it works great, but if the app is already started and I use a deep linking, it doesn't work (the screen doesn't change).
here is my code:
import React, { useState, useEffect } from "react";
import { NavigationContainer } from "#react-navigation/native";
import * as Linking from "expo-linking";
import useStore from "../Store";
import AuthStack from "./AuthStack";
import AppTabs from "./AppTabs";
const prefix = Linking.createURL("/");
export default function Routes() {
const [data, setData] = useState();
// These are screens for the AuthStack only
const linking = {
prefixes: [prefix],
config: {
screens: {
Login: "login",
Register: "register",
EmailValidation: "emailvalidation",
},
},
};
const handleDeepLink = (event) => {
const eventData = Linking.parse(event.url);
setData(eventData);
};
useEffect(() => {
const getInitialUrl = async () => {
const initialURL = await Linking.getInitialURL();
if (initialURL) {
setData(Linking.parse(initialURL));
}
};
Linking.addEventListener("url", handleDeepLink);
if (!data) {
getInitialUrl();
}
return () => {
Linking.removeEventListener("url", handleDeepLink);
};
}, []);
// Assuming the user variable is set to null or false
return (
<NavigationContainer linking={linking}>
{user ? <AppTabs /> : <AuthStack />}
</NavigationContainer>
);
}
Here is the auth stack screen:
import React from "react";
import { createStackNavigator } from "#react-navigation/stack";
import Login from "../Screens/Login";
import Register from "../Screens/Register";
import EmailValidation from "../Screens/EmailValidation";
const Stack = createStackNavigator();
export default function AuthStack() {
return (
<Stack.Navigator
screenOptions={{
header: () => null,
}}
initialRouteName="Login"
>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Register" component={Register} />
<Stack.Screen name="EmailValidation" component={EmailValidation} />
</Stack.Navigator>
);
}
Thanks for reading and ask me if you need more information/specification.
I think you forgot importing the data variable on the useEffect array deps, so the value of data isn't been updated properly
useEffect(() => {
const getInitialUrl = async () => {
const initialURL = await Linking.getInitialURL();
if (initialURL) {
setData(Linking.parse(initialURL));
}
};
Linking.addEventListener("url", handleDeepLink);
if (!data) {
getInitialUrl();
}
return () => {
Linking.removeEventListener("url", handleDeepLink);
};
}, [data]);

Isomorphic-style-loader, Cannot read property 'apply' of null

Hi I've seen this same error and multiple possible solutions but none has been able to solve my issue (Probably because I'm lacking in depth understanding of the whole React structure).
I know that context.insertCss.apply(context, styles); isn't receiving the context and that's why the error is thrown, I've added the ContextProvider but I'm afraid this could be conflicting with my routing setup. Also used Daniel's answer to this question [Why does isomorphic-style-loader throw a TypeError: Cannot read property 'apply' of undefined when being used in unison with CSS-Modules
server index.js
app.get('/*', (req, res) => {
const matchingRoutes = matchRoutes(Routes, req.url);
let promises = [];
matchingRoutes.forEach(route => {
if (route.loadData) {
promises.push(route.loadData());
}
});
// promise.then(data => {
Promise.all(promises).then(dataArr => {
// Let's add the data to the context
// const context = { data };
// const context = { dataArr };
const css = new Set()
const context = { insertCss: (...styles) => styles.forEach(style => css.add(style._getCss()))}
const app = React.renderToString(
<StaticRouter location={req.url}>
<ContextProvider context={context}>
<App/>
</ContextProvider>
</StaticRouter>
)
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf8', (err, indexData) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
if (context.status === 404) {
res.status(404);
}
if (context.url) {
return res.redirect(301, context.url);
}
return res.send(
indexData
.replace('<style id="myStyle"></style>',`<style type="text/css" id="myStyle">${[...css].join('')}</style>`)
.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
.replace(
'</body>',
`<script>window.__ROUTE_DATA__ = ${serialize(dataArr)}</script></body>`
)
);
});
});
});
Added on the server the ContextProvider in the renderToString(..) method, also I'm replacing the html body so the received CSS is attached to the HTML response.
ContextProvider.js
import React from 'react';
import PropTypes from 'prop-types'
import App from './App'
class ContextProvider extends React.Component {
static childContextTypes = {
insertCss: PropTypes.func,
}
getChildContext() {
return {
...this.props.context
}
}
render() {
return <App {
...this.props
}
/>
}
}
export default ContextProvider
Used the context provider from Daniel's answer (Reference above)
Client index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import ContextProvider from './ContextProvider';
const context = {
insertCss: (...styles) => {
const removeCss = styles.map(x => x._insertCss());
return () => {
removeCss.forEach(f => f());
};
},
}
ReactDOM.hydrate(
<BrowserRouter>
<ContextProvider context={context} />
</BrowserRouter>,
document.getElementById('root')
);
Passing the context through the ContextProvider as supposed.
App.js used inside the ContextProvider
import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';
import Routes from './routes';
export default props => {
return (
<div>
<ul>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/todos">Todos</NavLink>
</li>
<li>
<NavLink to="/posts">Posts</NavLink>
</li>
</ul>
<Switch>
{renderRoutes(Routes)}
</Switch>
</div>
);
};
Home.js where I'm trying to test the custom style
import React from 'react';
import withStyles from '../../node_modules/isomorphic-style-loader/withStyles'
import styles from '../scss/Home.scss';
function Home(props, context) {
return (
<h1>Hello, world!</h1>
)
}
export default withStyles(styles)(Home);
routes.js describes the routes used.
import Home from './components/Home';
import Posts from './components/Posts';
import Todos from './components/Todos';
import NotFound from './components/NotFound';
import loadData from './helpers/loadData';
const Routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/posts',
component: Posts,
loadData: () => loadData('posts')
},
{
path: '/todos',
component: Todos,
loadData: () => loadData('todos')
},
{
component: NotFound
}
];
export default Routes;
Almost sure there is an easy fix for this issue but it doesn't seem so trivial to me. Thank you in advance.
Please try to use the built in StyleContext of isomorphic-style-loader instead of custom context provider.
server.js:
import StyleContext from 'isomorphic-style-loader/StyleContext';
const insertCss = (...styles) => {
const removeCss = styles.map(style => style._insertCss());
return () => removeCss.forEach(dispose => dispose());
};
ReactDOM.render(
<StyleContext.Provider value={{ insertCss }}>
<Router>{renderRoutes(Routes)}</Router>
</StyleContext.Provider>,
document.getElementById('root')
);
client.js:
app.get('/*', function(req, res) {
const context = {};
const css = new Set(); // CSS for all rendered React components
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()));
const component = ReactDOMServer.renderToString(
<StyleContext.Provider value={{ insertCss }}>
<StaticRouter location={req.url} context={context}>
{renderRoutes(Routes)}
</StaticRouter>
</StyleContext.Provider>
);
if (context.url) {
res.writeHead(301, { Location: context.url });
res.end();
} else {
res.send(Html('React SSR', component));
}
});
You can see example project here: https://github.com/digz6666/webpack-loader-test/tree/ssr-2

React/Redux: State is updated in Redux object, but React component doesn't re-render

Tried to look through similar questions, but didn't find similar issues.
I am trying to implement sorts by name and amount in my app, this event is triggered in this component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { sortByExpenseName, sortByExpenseAmount } from '../actions/expensesFilters';
class ExpensesListFilter extends Component {
onSortByExpenseName = () => {
this.props.sortByExpenseName();
};
onSortByExpenseAmount = () => {
this.props.sortByExpenseAmount();
}
render() {
return (
<div>
<span>Expense Name</span>
<button onClick={this.onSortByExpenseName}>Sort me by name</button>
<button onClick={this.onSortByExpenseAmount}>Sort me by amount</button>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
sortByExpenseName: () => dispatch(sortByExpenseName()),
sortByExpenseAmount: () => dispatch(sortByExpenseAmount()),
});
export default connect(null, mapDispatchToProps)(ExpensesListFilter);
for that I am using following selector:
export default (expenses, { sortBy }) => {
return expenses.sort((a, b) => {
if (sortBy === 'name') {
return a.name < b.name ? 1 : -1;
} else if (sortBy === 'amount') {
return parseInt(a.amount, 10) < parseInt(b.amount, 10) ? 1 : -1;
}
});
};
I run this selector in mapStateToProps function for my ExpensesList component here:
import React from 'react';
import { connect } from 'react-redux';
import ExpensesItem from './ExpensesItem';
// my selector
import sortExpenses from '../selectors/sortExpenses';
const ExpensesList = props => (
<div className="content-container">
{props.expenses && props.expenses.map((expense) => {
return <ExpensesItem key={expense.id} {...expense} />;
}) }
</div>
);
// Here I run my selector to sort expenses
const mapStateToProps = (state) => {
return {
expenses: sortExpenses(state.expensesData.expenses, state.expensesFilters),
};
};
export default connect(mapStateToProps)(ExpensesList);
This selector updates my filter reducer, which causes my app state to update:
import { SORT_BY_EXPENSE_NAME, SORT_BY_EXPENSE_AMOUNT } from '../actions/types';
const INITIAL_EXPENSE_FILTER_STATE = {
sortBy: 'name',
};
export default (state = INITIAL_EXPENSE_FILTER_STATE, action) => {
switch (action.type) {
case SORT_BY_EXPENSE_NAME:
return {
...state,
sortBy: 'name',
};
case SORT_BY_EXPENSE_AMOUNT:
return {
...state,
sortBy: 'amount',
};
default:
return state;
}
};
Sort event causes my state to update, the expenses array in my expenses reducer below is updated and sorted by selector, BUT the ExpensesList component doesn't re-render after my expenses array in state is updated.
What I want my ExpensesList component to do, is to re-render with sorted expenses array and sort ExpensesItem components in list.
What could be the reason why it fails? Pretty sure I am missing out something essential, but can't figure out what. My expenses reducer:
import { FETCH_EXPENSES } from '../actions/types';
const INITIAL_STATE = {};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_EXPENSES:
return {
...state,
expenses: action.expenses.data,
};
default:
return state;
}
};
All these components are childs to this parent component:
import React from 'react';
import ExpensesListFilter from './ExpensesListFilter';
import ExpensesList from './ExpensesList';
const MainPage = () => (
<div className="box-layout">
<div className="box-layout__box">
<ExpensesListFilter />
<ExpensesList />
</div>
</div>
);
export default MainPage;
App.js file (where I run startExpenseFetch)
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import 'normalize.css/normalize.css';
import AppRouter, { history } from './routers/AppRouter';
import configureStore from './store/configureStore';
import LoadingPage from './components/LoadingPage';
import { startExpenseFetch } from './actions/expensesData';
import './styles/styles.scss';
const store = configureStore();
const jsx = (
<Provider store={store}>
<AppRouter />
</Provider>
);
let hasRendered = false;
const renderApp = () => {
if (!hasRendered) {
ReactDOM.render(jsx, document.getElementById('app'));
hasRendered = true;
}
};
store.dispatch(startExpenseFetch()).then(() => {
renderApp();
});
ReactDOM.render(<LoadingPage />, document.getElementById('app'));
Rest of files:
ExpenseItem Component:
import React from 'react';
const ExpenseItem = ({ amount, name }) => (
<div>
<span>{name}</span>
<span>{amount}</span>
</div>
);
export default ExpenseItem;
Action creators:
expensesData.js
import axios from 'axios';
import { FETCH_EXPENSE } from './types';
// no errors here
const ROOT_URL = '';
export const fetchExpenseData = expenses => ({
type: FETCH_EXPENSE,
expenses,
});
export const startExpenseFetch = () => {
return (dispatch) => {
return axios({
method: 'get',
url: `${ROOT_URL}`,
})
.then((response) => {
dispatch(fetchExpenseData(response));
console.log(response);
})
.catch((error) => {
console.log(error);
});
};
};
expensesFilters.js
import { SORT_BY_EXPENSE_NAME, SORT_BY_EXPENSE_AMOUNT } from './types';
export const sortByExpenseName = () => ({
type: SORT_BY_EXPENSE_NAME,
});
export const sortByExpenseAmount = () => ({
type: SORT_BY_EXPENSE_AMOUNT,
});
configureStores.js file
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import expensesDataReducer from '../reducers/expensesData';
import expensesFilterReducer from '../reducers/expensesFilters';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default () => {
const store = createStore(
combineReducers({
expensesData: expensesDataReducer,
expensesFilters: expensesFilterReducer,
}),
composeEnhancers(applyMiddleware(thunk))
);
return store;
};
AppRouter.js file
import React from 'react';
import { Router, Route, Switch, Link, NavLink } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory';
import MainPage from '../components/MainPage';
import NotFoundPage from '../components/NotFoundPage';
export const history = createHistory();
const AppRouter = () => (
<Router history={history}>
<div>
<Switch>
<Route path="/" component={MainPage} exact={true} />
<Route component={NotFoundPage} />
</Switch>
</div>
</Router>
);
export default AppRouter;
Don't you have a typo on your call to your selector? :)
// Here I run my selector to sort expenses
const mapStateToProps = (state) => {
return {
expenses: sortExpenses(state.expensesData.expenses, state.expnsesFilters),
};
};
state.expnsesFilters look like it should be state.expensesFilters
Which is one of the reasons you should make your sortExpenses selector grab itself the parts of the state it needs and do it's job on its own. You could test it isolation and avoid mistakes like this.
I found a reason why it happens, in my selector I was mutating my app's state. I wasn't returning a new array from it, and was changing the old one instead, that didn't trigger my vue layer to re-render. Fixed it and it works now.

Categories

Resources