Taken two pages back since render function is called twice - react-router

I want to go back to the previous page when Apollo Client error.graphQLErrors has an error with a specific message from the back-end server,
Below is the snippet of my code.
const Detail = () => { const { snackbar } = useSnackbar();
const history = useHistory();
return(
<Compo query={graphQLQuery}>
{({ data, error, }) => {
if(error?.graphQLErrors[0]?.extensions?.debugMessage.includes('Specific Error')){
history.goBack();
snackbar('Specific Error');
return <></>;
}
else{
//render another component
}
}
}
</Compo>);
Issue is since the render is called twice, when the error happens, history.goBack() is executed twice and I'm taken two pages back.
I'm able to avoid this by removing <React.StrictMode> encapsulating <App> component.
Is there a better way to do this?
I'm trying to avoid removing <React.StrictMode> since it's been there since a long time.

Issue
The issue here is that you are issuing an unintentional side-effect from the render method. In React function components the entire function body is considered to be the "render" method. Move all side-effects into a useEffect hook.
Solution
Since the code is using a children function prop you'll need to abstract what the "child" is rendering into a React component that can use React hooks.
Example:
const DetailChild = ({ data, error }) => {
const history = useHistory();
const { snackbar } = useSnackbar();
const isErrorCondition = error?.graphQLErrors[0]?.extensions?.debugMessage.includes('Specific Error'
useEffect(() => {
if (isErrorCondition)) {
history.goBack();
snackbar('Specific Error');
}
}, [error]);
return isErrorCondition
? null
: (
... render another component ...
);
};
...
const Detail = () => {
return (
<Compo query={graphQLQuery}>
{({ data, error }) => <DetailChild {...{ data, error }} />}
</Compo>
);
};

Related

How to create childElement from server response in a react app

I created a react app with many nested routes.
One of my nested route is using a backend api that returns a complete HTML content.
And I need to display that exact content with same HTML and styling in my UI.
I'm able to successfully achieve it by manipulating the DOM according to axios response using createElement and appendChild inside useEffect method.
But, the whole philosophy behind using react is, to NOT modify the DOM and let react work on it by simly updating the states or props.
My question is:
Is there a cleaner way to use api returned HTML in a react app?
Here is sample relevant code:
Item.js
...
...
useEffect( ()=>{
const fetchAndUpdateItemContent = async () => {
try {
const response = await axios.get(url);
var contentDiv = document.createElement("div");
contentDiv.innerHTML = response.data.markup;
document.getElementById(‘itemContent’)
.appendChild(contentDiv);
} catch (err) {
.....
console.error(err);
......
}
}
};
fetchAndUpdateItemContent();
},[itemId])
return (
<div id=‘itemContent'/>
);
}
What did NOT work
Ideally I should be able to have a state as itemContent in Item.js and be able to update it based upon server response like this. But when I do something like below, whole HTML markup is displayed instead of just the displayable content.
const [itemContent, setItemContent] = useState(‘Loading ...');
...
useEffect( ()=>{
const fetchAndUpdateItemContent = async () => {
try {
const response = await axios.get(url);
setItemContent(response.data.markup)
} catch (err) {
.....
console.error(err);
......
}
}
};
fetchAndUpdateItemContent();
},[itemId])
return (
<div id=‘itemContent'>
{itemContent}
</div>
You're actually trying to convert an HTML string to a JSX. You can assign it into react component props called dangerouslySetInnerHTML
Eg:
const Item = () => {
const yourHtmlStringResponse = '<h1>Heading 1</h1><h2>Heading 2</h2>'
return <div dangerouslySetInnerHTML={{__html: yourHtmlStringResponse}}></div>
}
You can try it here dangerouslySetInnerHTML-Codesandbox
I believe you can use dangerouslySetInnerHTML

TypeScript - Navigate dropdown listitems using keyboard

I'm working on an open-source project and have encountered a bug. I'm not able to navigate the dropdown list items using the keyboard (arrow key/tab). I've written the keyboard-navigation logic, but not quite sure of how to implement it. Below is the code snippet.
.
.
.
const TopNavPopoverItem: FC<ComponentProps> = ({closePopover, description, iconSize, iconType, title, to}) => {
const history = useHistory();
const handleButtonClick = (): void => {
history.push(to);
closePopover();
};
const useKeyPress = function (targetKey: any) { // where/how am I supposed to use this function?
const [keyPressed, setKeyPressed] = useState(false);
function downHandler(key: any) {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = (key: any) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
React.useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
});
return keyPressed;
};
return (
<button className="TopNavPopoverItem" onClick={handleButtonClick}>
<Icon className="TopNavPopoverItem__icon" icon={iconType} size={iconSize} />
<div className="TopNavPopoverItem__right">
<span className="TopNavPopoverItem__title">{title}</span>
<span className="TopNavPopoverItem__description">{description}</span>
</div>
</button>
);
};
Any workaround or fixes?
Thanks in advance.
A custom hook should always be defined at the top level of your file. It cannot be inside of a component. The component uses the hook, but doesn't own the hook.
You have a hook which takes a key name as an argument and returns a boolean indicating whether or not that key is currently being pressed. It's the right idea, but it has some mistakes.
When you start adding better TypeScript types you'll see that the argument of your event listeners needs to be the event -- not the key. You can access the key as a property of the event.
(Note: Since we are attaching directly to the window, the event is a DOM KeyboardEvent rather than a React.KeyboardEvent synthetic event.)
Your useEffect hook should have some dependencies so that it doesn't run on every render. It depends on the targetKey. I'm writing my code in CodeSandbox where I get warnings about "exhaustive dependencies", so I'm also adding setKeyPressed as a dependency and moving the two handlers inside the useEffect.
I see that you have one handler as function and one as a const. FYI it really doesn't matter which you use in this case.
Our revised hook looks like this:
import { useState, useEffect } from "react";
export const useKeyPress = (targetKey: string) => {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(
() => {
const downHandler = (event: KeyboardEvent) => {
if (event.key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = (event: KeyboardEvent) => {
if (event.key === targetKey) {
setKeyPressed(false);
}
};
// attach the listeners to the window.
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// remove the listeners when the component is unmounted.
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
},
// re-run the effect if the targetKey changes.
[targetKey, setKeyPressed]
);
return keyPressed;
};
I don't know you intend to use this hook, but here's a dummy example. We show a red box on the screen while the spacebar is pressed, and show a message otherwise.
Make sure that the key name that you use when you call the hook is the correct key name. For the spacebar it is " ".
import { useKeyPress } from "./useKeyPress";
export default function App() {
const isPressedSpace = useKeyPress(" ");
return (
<div>
{isPressedSpace ? (
<div style={{ background: "red", width: 200, height: 200 }} />
) : (
<div>Press the Spacebar to show the box.</div>
)}
</div>
);
}
CodeSandbox Link

Batching with useQuery react hooks getting back undefined

I am currently working on a project which requires me to make multiple queries/mutations. I tried setting up my apollo client with BatchHttpLink and I can see the data I am requesting in the network tab in the browser. It is coming back at an array of objects instead of JSON.
But the issue is when I try to grab the data in my component data is undefined. I tried using HttpLink instead of BatchHttpLink and I can get the data back from the hook.
My suspicion is the shape of the object that comes back from the response is different, I tried looking into documentation but I can't find much about batching.
Currently using "#apollo/client#^3.0.2"
Here's my client set up.
import { ApolloClient, InMemoryCache, ApolloLink, from } from '#apollo/client'
import { BatchHttpLink } from '#apollo/client/link/batch-http'
import { onError } from '#apollo/client/link/error'
const BASE_URL = 'http://localhost:4000'
const httpLink = new BatchHttpLink({
uri: BASE_URL,
credentials: 'include',
})
const csrfMiddleware = new ApolloLink((operation, forward) => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
'X-CSRF-Token': getCSRFToken(),
},
}))
return forward(operation)
})
const errorMiddleware = onError(({ networkError }) => {
if (networkError && 'statusCode' in networkError && networkError.statusCode === 401) {
window.location.assign('/accounts/login')
}
})
const client = new ApolloClient({
link: from([errorMiddleware, csrfMiddleware, httpLink]),
cache: new InMemoryCache(),
})
This is the react hook I'm trying to console log.
const {data} = useQuery(GET_USER_PERMISSIONS_AND_PREFERENCES)
Figured it out. You need to add another middleware to return the data that the useQuery hook can recognize. The data that comes back in the batch call is an array of objects shaped
{
payload: {
data: { ... }
}
}
So something like this did the trick for me
const batchParseMiddleware = new ApolloLink((operation, forward) => {
return forward(operation).map((data: any) => data.payload)
})
I have been having a similar issue, and have so far only been able to solve it by breaking batching and converting to a normal HttpLink

React render compoents by string (comming from server)

I have a list of components coming from the server with JSON which is inside a variable.
I want to call the component dynamically.
listOfcomponents.map(x => {
<Route path={x.slug} component={x.component} />;//(SomeComponent)
});
this is the component
export const SomeComponent = (props) => {
return <div>Some Component</div>
}
The server is deciding about the component that will be called.
How do I let Route have a dynamic component?
Thanks
This is possible! What you're looking for is called: Dynamic imports. And it rocks!
I've had a similar case where I needed to render components dynamically. However the biggest challenge was to keep my routes organized. This is how I implemented this logic:
async function getComponent(route) {
const {default: module} = await import(`../${route}`)
const element = document.createElement('div')
element.innerHTML = module.render()
return element
}

display json data with react and redux

I am attempting to load some local json data with redux and display in react app. But i'm getting the pageId is undefined in the reducer.
Not sure what I am doing wrong here, I think it might be something wrong with how I'm passing the data but im very new to redux so i'm not sure.
Data
const page = [
{"title":"Mollis Condimentum Sem Ridiculus"},
{"title":"Pharetra Tellus Amet Commodo"}
]
export default page;
Action
const getPage = (pageId) => {
const page = { pageId: pageId }
return {
type: 'GET_PAGE_SUCCESS',
payload: page
}
}
export default getPage
Reducer
import getPage from '../actions/actionCreators'
import pageData from './../data/pageData';
const defaultState = pageData
const pageReducer = (state = defaultState, action) => {
if (action.type = 'GET_PAGE_SUCCESS') {
state.page[action.payload.pageId].title = action.payload
}
return state
}
export default PageReducer
Component
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import getpage from '../../actions/actionCreators'
const mapStateToProps = (state, props) => {
const page = state.page[props.pageId]
return { page }
}
class Page extends Component {
componentDidMount () {
this.props.getpage(this.props.pageId)
}
render() {
return (<div><PageContainer pageId={0} /></div>)
}
}
const PageContainer = connect(mapStateToProps, { getpage })(page)
export default Page
I've modified your code into a working JSFiddle for reference: https://jsfiddle.net/qodof048/11/
I tried to keep it as close to your example, but let me explain the changes I made to get it working (also note that JSFiddle does not use the ES6 import syntax).
1) Your PageContainer was not constructed correctly. The last parameter should have been a reference to the Page component (not 'page').
const PageContainer = connect(mapStateToProps, { getPageSimple, getPageAsync })(PageComponent)
2) You used PageContainer in the Page component, but PageContainer is the 'wrapper' around Page. You use PageContainer instead of Page in your render method, so it loads the data (maps state and actions).
ReactDOM.render(
<Provider store={store}>
<div>
<PageContainer pageId="0" async={false} />
<PageContainer pageId="1" async={true} />
</div>
</Provider>,
document.getElementById('root')
);
3) The store was mixed up a bit. If I understood your example correctly you want to load a page into the local store from the pageData array, which simulates a server call maybe. In that case you intialState can't be pageData, but rather is an empty object. Think of it like a local database you're going to fill. The call to your action getPage then gets the page (here from your array) and dispatches it into the store, which will save it there.
const getPageSimple = (pageId) => {
const page = pageDatabase[pageId]; // this call would be to the server
// then you dispatch the page you got into state
return {
type: 'GET_PAGE_SUCCESS',
payload: {
id: pageId,
page: page
}
}
}
4) I've added an async example to the JSFiddle to explain how you would actually fetch the page from the server (since the simple example would not be sufficient). This needs the thunk middleware for redux to work (since you need access to the dispatch method in order to async call it). The setTimeout simulates a long running call.
const getPageAsync = (pageId)=>{
return (dispatch, getState) => {
setTimeout(()=>{
const page = pageDatabase[pageId]; // this call would be to the server, simulating with a setTimeout
console.log("dispatching");
// then you dispatch the page you got into state
dispatch({
type: 'GET_PAGE_SUCCESS',
payload: {
id: pageId,
page: page
}
});
}, 2000);
}
}
The JSFiddle loads 2 containers, one with your simple getPage and one with the async version, which loads the title after 2 seconds.
Hope that helps you along on your react/redux journey.
Hey I see a small mistake in you component, I think. You are doing this.props.pageId, when you are setting page and not pageId on the component's props. So shouldn't it be this.props.getPage(this.props.page.pageId) instead? Could that be it?
Also a small side note, an important tip for using redux is to not mutate state. In you reducer where you are doing state.page[action.payload.pageId].title = action.payload you should probably not set state like that, but instead return a new object called newState which is identical to state, but with the title updated. It is important to treat objects as immutable in Redux. Cheers