Isomorphic react redux sending Dynamic Id - reactjs

I am using react router with redux for isomorphic app. I have an use case where href needs to work both on client and server side. This is mainly for SEO reasons. for bots it should use href and when user clicks it should work too
my route looks like this
<Route name="app" component={ProductPage} path="/product/:Id">
<IndexRoute component={ProductContainer}/>
</Route>
http://localhost:3000/product/1234 (this works from server side)
Once the above URL renders data, i have page with different URLS of that kind
http://localhost:3000/product/3456
http://localhost:3000/product/9999
when user clicks the above URLS, i don't want to do a post back, i want to use browserHistory api and render the component, is it possible?
I have this in the container
static needs = [
productActions.getProductData
]
Any direction will be appreicated

Server Side
You could require a function call that would do your dispatches onEnter of the specific route, which would populate the app state that you need to render your component.
the route would look something like this:
<Route component={ProductPage} path="/product/:Id" onEnter={requireProductData} />
requireProductData would be a function in your main.js something like this:
function requireProductData(nextState, replace, callback) {
const { params } = nextState
var idNum = parseInt(params.id.toString().split("-")[0])
const currentState = store.getState()
if (idNum > 0) {
store.dispatch(ProductActionCreators.productInfo({ id: idNum })).then((event) => {
callback()
}, (error) => {
callback()
})
} else {
callback()
}
}
this would do your action creator calls and then your reducer would take care of the app state. then you could render your component from that url.
Client Side
Yeah, you're right about doing an onClick handler. Here's an example of where clicking a button causes an API call with a dispatch:
class SubscribeUserDialog extends Component {
subscribeUser(privacy) {
const { dispatch, readingPlan, auth } = this.props
if (!auth.isLoggedIn) window.location.replace(`/sign-in`)
// if user isn't subscribed, then subscribe!
if (!readingPlan.subscription_id) {
dispatch(ActionCreators.readingplanSubscribeUser({ id: readingPlan.id , private: privacy }, auth.isLoggedIn)).then(() => {
// redirect to plan
this.goToPlan()
})
} else {
// user already subscribed
this.goToPlan()
}
}
render() {
return (
<div className='plan-privacy-buttons text-center'>
<p className='detail-text'><FormattedMessage id="plans.privacy.visible to friends?" /></p>
<div className='yes-no-buttons'>
<a className='yes solid-button green' onClick={this.subscribeUser.bind(this, false)}><FormattedMessage id="ui.yes button"/></a>
<a className='no solid-button gray' onClick={this.subscribeUser.bind(this, true)}><FormattedMessage id="ui.no button" /></a>
</div>
</div>
)
}
}
export default SubscribeUserDialog
In your case, you would have the onClick attached to your links.
Does this help you?

Related

Props lost after navigate and cloneElement

In an app I'm currently working on, the authentification has been done like this, to pass user's data into children components (in App.js, I would have rather used useContext to access user data in whatever component) [App.js]:
const RequireAuth = ({ children }) => {
// fetch user data from database, ...
return <>{React.cloneElement(children, { user: {...user, ...userExtraData} })}</>;
};
Then, an example of a Route is specified as [App.js]:
<Route
path="/addgame"
element={
<RequireAuth>
<FormAddGame />
</RequireAuth>
}
/>
However, my current problem is the following:
From a ComponentA, I want to navigate to /addgame (=FormAddGame component, see below) while setting an existingGame prop. Thus, I use [ComponentA.js]:
let navigate = useNavigate()
navigate('addgame', { existingGame: game })
The said FormAddGame component is [FormAddGame.js]:
function FormAddGame({ user }) {
const { existingGame } = useLocation()
console.log(existingGame)
...
}
export default FormAddGame;
However, while I correctly navigate to /addgame, the existingGame prop stays undefined once in FormAddGame while it should be not (as game is not undefined)
Try passing props like this:
navigate("addgame", { state: { existingGame: game } });
and destructure like this:
const { state: { existingGame } = {} } = useLocation();

Elasticsearch search-ui with React

I have a React site with aside and main content. I want to use search-ui for searching on the site.
The search bar should be on the aside, and when the user searches for something, results should be displayed on the main content. Aside and main content are two separated react components.
In my aside, I'm configuring search-ui SearchBox like this
<SearchBox
autocompleteResults={{
titleField: "title",
urlField: "url"
}}
autocompleteSuggestions={true}
onSubmit={searchTerm => {
navigate("/elastic-search?q=" + searchTerm);
}}
onSelectAutocomplete={(selection, {}, defaultOnSelectAutocomplete) => {
if (selection.suggestion) {
navigate("/elastic-search?q=" + selection.suggestion);
} else {
defaultOnSelectAutocomplete(selection);
}
}}
/>
So when the user searches something the app will redirect to a separate page named elastic-search and I'm passing the searchTerm in the URL through navigate method.
On MainContent I have results like this:
<Results titleField='title' urlField='url'/>
Now the question is how can I fetch searchTerm and display the results on main content. The structure of the app is like this:
<App>
<SearchProvider config={config}>
<Aside /> ---- Here I have <SearchBox>
<MainContent /> ---- Here I have <Results>
</SearchProvider>
</App>
When I search the app redirects to /elastic-search with searchTerm in URL, but the results are not displaying. If I refresh the page they are displayed. How can I notify Results or re-render the page, so I can show the searched results.
Your Results seems to be missing some parameters and should look something like this:
<>
<Results
titleField="title"
urlField=""
view={SearchView}
resultView={SearchResultView}
/>
</>
And your SearchView (Used to override the default view for this Component.) and SearchResultView (Used to override individual Result views.) components, should look something like this:
const SearchView = ({ children }) => {
return <div>{children}</div>
};
const SearchResultView = ({ result: searchResult }) => {
return <div>{searchResult.content}</div>
}
Additional suggestion
This is a working example in the Next.js app with import { useRouter } from "next/router"; that needs to be replaced with your routing solution. In the SearchBox component:
export const SearchBoxComponent = () => {
const router = useRouter();
return (
<>
<SearchBox
searchAsYouType={true}
autocompleteResults={{
titleField: "title",
urlField: "",
shouldTrackClickThrough: true,
clickThroughTags: ["test"],
}}
autocompleteSuggestions={true}
onSubmit={(searchTerm) => {
const urlEncodedQuery = encodeURI(searchTerm).replace(/%20/g, "+");
router.push(`/search?q=${urlEncodedQuery}`);
}}
...
</>
)
}

How to add to a parent event handler in React

I have a React page setup root container page with a global Header component and some child components (via React Router, but that might not be relevant). The Header component has buttons that need to do specific things (like navigate) but also need to have functionality dictated by the child components. I have looked around for information on callbacks and props, but I am at a loss on how to achieve this. (Note, I am also using Redux but my understanding is that you should not save functions in Redux state because they are not serializable).
A simplified version of my scenario:
// Container Page
const Container = () => {
const onNavigate = () => {
// How could Cat or Dog component add extra functionality here before navigate() is called?
navigate('/complete');
};
return (
<Header onButtonClick={onNavigate}>
<Switch>
<Route path='/cats' component={Cat} />
<Route path='/dogs' component={Dog} />
</Switch>
);
}
// Cat component
const Cat = (props) => {
const speakBeforeNavigating = () => {
// This needs to happen when the "Navigate" button in the Header is clicked
console.log("Meow!");
};
return (
<span>It is a cat</span>
);
}
My recommendation is that you define all of the callbacks in the parent component, which is why I had to double check with you that the callbacks don't need to access the internal state of the child components.
I would define the props for each Route individually, and include a callback along with the props.
const ROUTES = [
{
path: "/cats",
component: Cat,
callback: () => console.log("Meow!")
},
{
path: "/dogs",
component: Dog,
callback: () => console.log("Woof!")
}
];
// Container Page
const Container = () => {
// functionality is based on the current page
const match = useRouteMatch();
// need history in order to navigate
const history = useHistory();
const onNavigate = () => {
// find the config for the current page
const currentRoute = ROUTES.find((route) => route.path === match.path);
// do the callback
currentRoute?.callback();
// navigate
history.push("/complete");
};
return (
<>
<Header onButtonClick={onNavigate} />
<Switch>
{ROUTES.map((props) => (
<Route key={props.path} {...props} />
))}
</Switch>
</>
);
};
You can use context api and send it as prop. While sending it as a prop you can pass callback function to your onNavigate function. Like this
const onNavigate = (callback) => {
callback();
navigate('/complete');
}
And you use it like this
<button onClick={() => onNavigate(() => console.log('blabla'))}
For context api information I recommend you to check React official documentation.

How to update the url of a page using an input field?

I try to integrate a search page in my application with react router v5.
How could I update my url's query parameter using my search box?
When I refresh my application, I lose my search results and the value of my search field.
I use redux to manage the state of the value of my search fields and my search results, I think that going through the parameters of the url would be a better solution but I do not know how to do that.
I tried a solution (see my code), but the query parameter of the url is not synchronized with the value of my text field
Edit:
My component Routes.js
const Routes = (props) => {
return (
<div>
<Route exact path="/" component={Home} />
<Route
exact
path='/search'
component={() => <Search query={props.text} />}
/>
<Route path="/film/:id" component={MovieDetail} />
<Route path="/FavorisList" component={WatchList} />
<Route path="/search/:search" component={Search} />
<Route path="*" component={NotFound} />
</div>
)}
My component SearchBar.js (Embedded in the navigation bar, the Search route displays the search results)
EDIT:
I wish to realize the method used by Netflix for its research of series.
I want to be able to search no matter what page I am in, if there is an entry in the input field, I navigate to the search page withthis.props.history.push (`` search / ), if the input field is empty, I navigate to the page with this.props.history.goBack ().
The state inputChange is a flag that prevents me from pushing to the search page each time I enter a character.
To know more, I had opened = a post here => How to change the route according to the value of a field
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
inputValue:'',
};
}
setParams({ query }) {
const searchParams = new URLSearchParams();
searchParams.set("query", query || "");
return searchParams.toString();
}
handleChange = (event) => {
const query = event.target.value
this.setState({ inputValue: event.target.value})
if (event.target.value === '') {
this.props.history.goBack()
this.setState({ initialChange: true })
return;
}
if(event.target.value.length && this.state.initialChange){
this.setState({
initialChange:false
}, ()=> {
const url = this.setParams({ query: query });
this.props.history.push(`/search?${url}`)
this.search(query)
})
}
}
search = (query) => {
//search results retrieved with redux
this.props.applyInitialResult(query, 1)
}
render() {
return (
<div>
<input
type="text"
value={this.state.inputValue}
placeholder="Search movie..."
className={style.field}
onChange={this.handleChange.bind(this)}
/>
</div>
);
}
export default SearchBar;
Component App.js
class App extends React.Component {
render(){
return (
<div>
<BrowserRouter>
<NavBar />
<Routes/>
</BrowserRouter>
</div>
);
}
}
export default App;
Query for search results (Managed with Redux)
export function applyInitialResult(query, page){
return function(dispatch){
getFilmsFromApiWithSearchedText(query, page).then(data => {
if(page === 1){
dispatch({
type:AT_SEARCH_MOVIE.SETRESULT,
query:query,
payload:data,
})
}
})
}
}
Instead of splitting up the routes, you could just use an optional param and handle the query or lack thereof in the component by changing <Route path="/search/:search" component={Search} /> to <Route path="/search/:search?" component={Search} /> and removing <Route exact path='/search' component={() => <Search query={props.text} />} /> entirely.
With that change, you can then get the current query by looking at the value of props.match.params.search in this component. Since you're updating the URL each time the user changes the input, you then don't need to worry about managing it in the component state. The biggest issue with this solution is you'll probably want to delay the search for a little bit after render, otherwise you'll be triggering a call on every keystroke.
EDITED IN RESPONSE TO QUESTION UPDATE
You're right, if applyInitialResult is just an action creator, it won't be async or thenable. You still have options, though.
For example, you could update your action creator so it accepts callbacks to handle the results of the data fetch. I haven't tested this, so treat it as pseudocode, but the idea could be implemented like this:
Action creator
export function applyInitialResult(
query,
page,
// additional params
signal,
onSuccess,
onFailure
// alternatively, you could just use an onFinished callback to handle both success and failure cases
){
return function(dispatch){
getFilmsFromApiWithSearchedText(query, page, signal) // pass signal so you can still abort ongoing fetches if input changes
.then(data => {
onSuccess(data); // pass data back to component here
if(page === 1){
dispatch({
type:AT_SEARCH_MOVIE.SETRESULT,
query:query,
payload:data,
})
}
})
.catch(err => {
onFailure(data); // alert component to errors
dispatch({
type:AT_SEARCH_MOVIE.FETCH_FAILED, // tell store to handle failure
query:query,
payload:data,
err
})
})
}
}
searchMovie Reducer:
// save in store results of search, whether successful or not
export function searchMovieReducer(state = {}, action) => {
switch (action.type){
case AT_SEARCH_MOVIE.SETRESULT:
const {query, payload} = action;
state[query] = payload;
break;
case AT_SEARCH_MOVIE.FETCH_FAILED:
const {query, err} = action;
state[query] = err;
break;
}
}
Then you could still have the results/errors directly available in the component that triggered the fetch action. While you'll still be getting the results through the store, you could use these sort of triggers to let you manage initialChange in the component state to avoid redundant action dispatches or the sort of infinite loops that can pop up in these situations.
In this case, your Searchbar component could look like:
class SearchBar extends React.Component {
constructor(props){
this.controller = new AbortController();
this.signal = this.controller.signal;
this.state = {
fetched: false,
results: props.results // <== probably disposable based on your feedback
}
}
componentDidMount(){
// If search is not undefined, get results
if(this.props.match.params.search){
this.search(this.props.match.params.search);
}
}
componentDidUpdate(prevProps){
// If search is not undefined and different from prev query, search again
if(this.props.match.params.search
&& prevProps.match.params.search !== this.props.match.params.search
){
this.search(this.props.match.params.search);
}
}
setParams({ query }) {
const searchParams = new URLSearchParams();
searchParams.set("query", query || "");
return searchParams.toString();
}
handleChange = (event) => {
const query = event.target.value
const url = this.setParams({ query: query });
this.props.history.replace(`/search/${url}`);
}
search = (query) => {
if(!query) return; // do nothing if empty string passed somehow
// If some search occurred already, let component know there's a new query that hasn't yet been fetched
this.state.fetched === true && this.setState({fetched: false;})
// If some fetch is queued already, cancel it
if(this.willFetch){
clearInterval(this.willFetch)
}
// If currently fetching, cancel call
if(this.fetching){
this.controller.abort();
}
// Finally queue new search
this.willFetch = setTimeout(() => {
this.fetching = this.props.applyInitialResult(
query,
1,
this.signal,
handleSuccess,
handleFailure
)
}, 500 /* wait half second before making async call */);
}
handleSuccess(data){
// do something directly with data
// or update component to reflect async action is over
}
handleFailure(err){
// handle errors
// or trigger fetch again to retry search
}
render() {
return (
<div>
<input
type="text"
defaultValue={this.props.match.params.search} // <== make input uncontrolled
placeholder="Search movie..."
className={style.field}
onChange={this.handleChange.bind(this)}
/>
<div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
// get results from Redux store based on url/route params
results: ownProps.match.params.search
? state.searchMovie[ownProps.match.params.search]
: []
});
const mapDispatchToProps = dispatch => ({
applyInitialResult: // however you're defining it
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchBar)
EDIT 2
Thanks for the clarification about what you're imagining.
The reason this.props.match.params is always blank is because that's only available to the Search component - the Searchbar is entirely outside the routing setup. It also renders whether or not the current path is /search/:search, which is why withRouter wasn't working.
The other issue is that your Search route is looking for that match param, but you're redirecting to /search?query=foo, not /search/foo, so match params will be empty on Search too.
I also think the way you were managing the initialChange state was what caused your search value to remain unchanged. You handler gets called on every change event for the input, but it shuts itself off after the first keystroke and doesn't turn on again until the input is cleared. See:
if (event.target.value === '') {
this.props.history.goBack()
this.setState({ initialChange: true }) // <== only reset in class
return;
}
...
if(event.target.value.length && this.state.initialChange){
this.setState({
initialChange:false
}, ()=> {
// etc...
})
}
This is what the pattern I was suggesting accomplishes - instead of immediately turning off your handler, set a delay for the dispatch it and keep listening for changes, searching only once user's done typing.
Rather than copying another block of code here, I instead made a working example on codesandbox here addressing these issues. It could still use some optimization, but the concept's there if you want to check out how I handled the Search page, SearchBar, and action creators.
The Searchbar also has a toggle to switch between the two different url formats (query like /search?query=foo vs match.param like /search/foo) so you can see how to reconcile each one into the code.

React Redux parent and child component sharing state URL keeps reloading old ID

so I have a redux store with has this basic structure:
{
user: {
id: 1,
currentCompanyId: 2,
}
companyDetails: {
id: 2,
name: 'Joes Company'
},
otherCompanies: [2,3,4],
}
I have a parent page which in the header has a dropdown that allows the user to link / switch to another company.
The parent component needs to know which company is selected so it can show the name in the heading.
The child component displays the details of the current company.
There will be various types of companies and the url's / sections will be different for each type. So in the child component I was trying to set the user's company and then load the details of that company.
The child component doesn't need to directly reference the user or current company.
So what I was doing was in the child component I would listen in willmount and willreceiveprops for a change to the url, then fire an action to update the user company. This will then cause the parent to re render as the user object has changed. Which in turn will create a new / remount the child component. So far this seemed logical.
The issue is that when I have selected company 2 and try to switch to company 3, it will set the company to 3, but then reset it back to 2 again.
I am not sure if this is to do with the URL having not updated or something. I have gone around in circles so much now that I am not sure what the workflow should be anymore.
edit
if I comment out this.loadContractorInfo(contractorId); from ContractorHome.js in componentWillMount() it will work correctly (i.e. the URL stays with the number in the link, vs reverting to the old one. I assume this is to do with redux actions are async, and although I am not doing any network calls it is some race condition between getting data for the contractor page and the contextheader wanting to display / update the current company
edit 2
so to confirm. I have loaded the page at the root of the site, all fine. I select a company / contractor from the dropdown. this loads fine. I go to change that selection to a different contractor from the dropdown. The first component to be hit will be the parent (contextheader), the location prop in nextprops will have updated to have the correct ID in the URL. the method execution at this point will NOT update any thing, no actions are fired. It then hits the child (contractorhome) willreceiveprops method, again the URL in location is good as is the match params. I have commented out all code in willrecieveprops so it does not do anything here either. It will then go back to the parent willreceive props and the location will have gone back to the previous ID in the URL.
app.js snippet:
render() {
return (
<div className="app">
<Router>
<div>
<div className="bodyContent">
<ContextHeader />
<Switch>
{/* Public Routes */}
<Route path="/" exact component={HomePage} />
<Route path="/contractor" component={ContractorRoute} />
<Route path="/building" component={BuildingRoute} />
</Switch>
</div>
<Footer />
</div>
</Router>
</div>
);
}
contextheader:
componentWillReceiveProps(nextProps) {
const { setCompany } = this.props;
const currentInfo = this.props.sharedInfo && this.props.sharedInfo.currentCompany;
const newInfo = nextProps.sharedInfo && nextProps.sharedInfo.currentCompany;
if (newInfo && newInfo.id && (!currentInfo || currentInfo.id !== newInfo.id)) {
setCompany(newInfo.id, newInfo.type);
}
}
render() {
const { user, companies, notifications } = this.props;
/* render things here */
}
);
}
}
const mapStateToProps = state => ({
user: state.user,
sharedInfo: state.sharedInfo,
companies: state.companies,
notifications: state.notifications,
});
const mapDispatchToProps = dispatch => ({
setCompany: (companyId, type) => dispatch(setCurrentCompany(companyId, type)),
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ContextHeader));
contractorRoute:
const ContractorRoute = ({ match }) => (
<Switch>
<Route path={`${match.path}/:contractorId`} component={ContractorHome} />
</Switch>
);
contractorHome
componentWillMount() {
const contractorId = parseInt(this.props.match.params.contractorId, 10);
this.setSharedCompany(contractorId);
this.loadContractorInfo(contractorId);
}
componentWillReceiveProps(nextProps) {
const newContractorId = parseInt(nextProps.match.params.contractorId, 10);
if (this.props.match.params.contractorId !== nextProps.match.params.contractorId) {
this.setSharedCompany(newContractorId);
}
}
setSharedCompany(contractorId) {
const { sharedInfo, setCompany } = this.props;
if (typeof contractorId === 'number') {
if (!sharedInfo || !sharedInfo.currentCompany || !sharedInfo.currentCompany.id || sharedInfo.currentCompany.id !== contractorId) {
setCompany(contractorId);
}
}
}
loadContractorInfo(contractorId) {
const { sharedInfo, getContractorInfo, busy } = this.props;
if (!busy && sharedInfo && sharedInfo.currentCompany && sharedInfo.currentCompany.id === contractorId) {
getContractorInfo(contractorId);
}
}
render() { /*render lots of things here*/};
const mapStateToProps = (state) => {
const selector = state.contractor.details;
return {
sharedInfo: state.sharedInfo,
details: selector.info,
error: selector.request != null ? selector.request.error : null,
busy: selector.request && selector.request.busy,
};
};
const mapDispatchToProps = dispatch => ({
getContractorInfo: contractorId => dispatch(getContractor(contractorId)),
setCompany: contractorId => dispatch(setSharedinfoCurrentCompany(contractorId, 'contractor')),
});
export default connect(mapStateToProps, mapDispatchToProps)(ContractorHome);

Resources