React Router conditional redirect - redirect

I'm building a search UI using React, React Router and the awesome Reactivesearch library. I'm trying to figure out how I can prevent users from simply navigating to mydomain.com/search, since that is my search results route.
Ideally, if users tried to navigate to mydomain.com/search, I will use RR Redirect component to redirect to the home page.
I'm using "/search" for the route that the Route component in RR(v5) to render the search results page and can't quite figure out how to use something like /search?q=${value} to render the page?
As a preface I do have this in the render block (I'm using class based component for search results)
let value = JSON.stringify(queryString.parse(location.search));
if (this.value === '' || null) {
return (
<Redirect to="/" />
);
}
However, its not working... I can still go to my address bar and type in mydomain.com/search and the page renders.
Here is an example in my SearchResults.tsx:
<Route path = "/search" render={() => (
<ReactiveList
...
...
/>
/>
I'm trying to get to
<Route path = `/search?q="${value}"` render={() => (
<ReactiveList
...
...
/>
/>
Update
Docs on ReactiveList
Example from docs:
<ReactiveList
componentId="SearchResult"
dataField="ratings"
pagination={false}
paginationAt="bottom"
pages={5}
sortBy="desc"
size={10}
loader="Loading Results.."
showResultStats={true}
renderItem={res => <div>{res.title}</div>}
renderResultStats={function(stats) {
return `Showing ${stats.displayedResults} of total ${stats.numberOfResults} in ${
stats.time
} ms`;
}}
react={{
and: ['CitySensor', 'SearchSensor'],
}}
/>
How can I prevent users from simply navigating to mydomain.com/search ??

If you want to conditionally render the ReactiveList component based on if there's a truthy q queryString parameter then you can use either a wrapper component, a layout route, or a Higher Order Component (HOC) to read the queryString and handle the redirection logic.
react-router-dom#6
Using a Wrapper
import { Navigate, useSearchParams } from 'react-router-dom';
const QuaryParametersWrapper = ({ children, parameters = [] }) => {
const [searchParams] = useSearchParams();
const hasParameter = parameters.some(
(parameter) => !!searchParams.get(parameter)
);
return hasParameter ? children : <Navigate to="/" replace />;
};
...
<Route
path="/search"
element={(
<ReactiveListWrapper parameters={["q"]}>
<ReactiveList
...
...
/>
</ReactiveListWrapper>
)}
/>
Using a custom HOC
import { Navigate, useSearchParams } from 'react-router-dom';
const withQueryParameters = (...parameters) => (Component) => (props) => {
const [searchParams] = useSearchParams();
const hasParameter = parameters.some(
(parameter) => !!searchParams.get(parameter)
);
return hasParameter ? <Component {...props} /> : <Navigate to="/" replace />;
};
...
export default withQueryParameters("q")(ReactiveList);
...
import ReactiveListWrapper from '../path/to/ReactiveList';
...
<Route path="/search" element={<ReactiveListWrapper />} />
Using a layout route
import { Navigate, Outlet, useSearchParams } from 'react-router-dom';
const QuaryParametersLayout = ({ parameters = [] }) => {
const [searchParams] = useSearchParams();
const hasParameter = parameters.some(
(parameter) => !!searchParams.get(parameter)
);
return hasParameter ? <Outlet /> : <Navigate to="/" replace />;
};
...
<Route element={<QuaryParametersLayout parameters={["q"]} />}>
<Route path="/search" element={<ReactiveList />} />
</Route>
Demo
react-router-dom#5
The useSearchParams hook doesn't exist in v5 so you can create your own.
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
const useSearchParams = () => {
const { search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
return [searchParams];
};
Using a Wrapper
import useSearchParams from '../path/to/useSearchParams';
const QuaryParametersWrapper = ({ children, parameters = [] }) => {
const [searchParams] = useSearchParams();
const hasParameter = parameters.some(
(parameter) => !!searchParams.get(parameter)
);
return hasParameter ? children : <Redirect to="/" />;
};
...
<Route
path="/search1"
render={(routeProps) => (
<QuaryParametersWrapper parameters={["q"]}>
<ReactiveList {...routeProps} />
</QuaryParametersWrapper>
)}
/>
Using a custom HOC
import { Redirect } from 'react-router-dom';
import useSearchParams from '../path/to/useSearchParams';
const withQueryParameters = (...parameters) => (Component) => (props) => {
const [searchParams] = useSearchParams();
const hasParameter = parameters.some(
(parameter) => !!searchParams.get(parameter)
);
return hasParameter ? <Component {...props} /> : <Redirect to="/" />;
};
...
export default withQueryParameters("q")(ReactiveList);
...
import ReactiveListWrapper from '../path/to/ReactiveList';
...
<Route path="/search2" component={QueryReactiveList} />
Demo

Related

React-native: How can I auto update the mainscreen after adding a new item

I'm basically new to React-native and I'm trying to integrate it with MongoDB, apollo-graphql to implement a basic chat app.
I need to update the screen automatically and show the newly created group when I add a new group. Now what happens is, when I create the group, I need to reload the app every time to show the updation made.
GroupScreen.tsx
const MY_GROUPS = gql`
query chatRooms {
chatRooms {
id
name
createdAt
imageUri
}
}
`;
export default function GroupScreen() {
const [groups, setGroups] = useState(null);
const { data, error, loading } = useQuery(MY_GROUPS);
useEffect(() => {
if (error) {
Alert.alert("Something went Wrong! Please reload.");
}
}, [error]);
useEffect(() => {
if (data) {
//console.log(data);
setGroups(data.chatRooms);
}
}, [data]);
return (
<View style={styles.container}>
<FlatList
style={{ width: "100%" }}
data={groups}
renderItem={({ item }) => <GroupListItem chatRoom={item} />}
keyExtractor={(item) => item.id}
/>
<NewGroupButtonItem />
</View>
);
}
NewGroupButtonItem.tsx
const CREATE_CHATROOM = gql`
mutation Mutation(
$createChatRoomName: String!
$createChatRoomImageUri: String
) {
createChatRoom(
name: $createChatRoomName
imageUri: $createChatRoomImageUri
) {
id
name
imageUri
createdAt
users {
id
name
}
}
}
`;
const NewGroupButtonItem = () => {
const [modalVisible, setModalVisible] = useState(false);
const [groupName, setGroupName] = useState("");
const [groupPic, setGroupPic] = useState(null);
const [newGroup, { data, error, loading }] = useMutation(CREATE_CHATROOM);
const onPress = () => {
setGroupName("");
setGroupPic(null);
setModalVisible(!modalVisible);
};
const onPressSave = () => {
newGroup({
variables: {
createChatRoomName: groupName,
createChatRoomImageUri: groupPic,
},
});
setModalVisible(!modalVisible);
};
return (
<View style={styles.container}>
<Modal animationType="fade" transparent={true} visible={modalVisible}>
<TouchableOpacity
style={styles.touchableContainer}
activeOpacity={1}
onPress={() => setModalVisible(!modalVisible)}
>
<View style={styles.mainContainer}>
<View style={styles.innerContainer}>
<Pressable
onPress={() => {
console.warn("Clicked Image!");
}}
>
<Image source={{}} style={styles.avatar} />
</Pressable>
<TextInput
placeholder={"Group Name"}
style={styles.inputBox}
value={groupName}
onChangeText={setGroupName}
/>
<Pressable
onPress={() => {
console.warn("Clicked Emojies!");
}}
>
<Entypo name="emoji-flirt" size={30} color="#37474f" />
</Pressable>
</View>
{!groupName ? (
<Text style={styles.saveButton} onPress={onPress}>
Cancel
</Text>
) : (
<Text style={styles.saveButton} onPress={onPressSave}>
Save
</Text>
)}
</View>
</TouchableOpacity>
</Modal>
<TouchableOpacity onPress={onPress}>
<MaterialIcons name="group-add" size={30} color="white" />
</TouchableOpacity>
</View>
);
};
export default NewGroupButtonItem;

IonRouter or react-router-dom Not Passing props.match via PrivateRoute

I am trying to access url params from inside a component in a project using IonReactRouter. For some reason the match param in props is just not present. All other props are present however.
I am using a PrivateRoute using what I believe to be the standard react-router-dom PrivateRoute implementation.
PrivateRoute:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
const PrivateRoute = ({ Component, ...rest }) => {
const { user } = rest;
return (
<Route
{...rest}
render={(props) => {
if (user !== null) {
// return React.cloneElement(children, { ...rest });
return <Component {...props} />
}
return (
<Redirect
to={{
pathname: "/",
state: { from: props.location },
}}
/>
);
}}
/>
);
};
const mapStateToProps = (state) => {
return {
user: state.auth.user,
};
};
export default connect(mapStateToProps)(PrivateRoute);
App.js routes:
return (
<IonApp className="app">
<IonReactRouter>
<Switch>
<Route
exact
path="/"
render={(props) => {
return appLoading == true && user == null ? (
<Loader />
) : user !== null ? (
<Dashboard {...props} />
) : (
<Auth />
);
}}
/>
<IonRouterOutlet>
<PrivateRoute exact path="/Dashboard/:id">
<Dashboard />
</PrivateRoute>
...
</IonRouterOutlet>
</Switch>
</IonReactRouter>
</IonApp>
);
};
Link to component extract:
return (
<ResultWrap key={i}>
<IonItem routerLink={`/Dashboard/${item.id}`}>
<SearchResult>{item.title}</SearchResult>
</IonItem>
<Type>
<TypeWrap>{item.type}</TypeWrap>
</Type>
</ResultWrap>
);
})
It seems that I was using the component incorrectly:
Wrong way:
<PrivateRoute exact path="/Dashboard/:id">
<Dashboard />
</PrivateRoute>
Right way:
<PrivateRoute exact path="/Dashboard/:id" component={Dashboard}>

Redux-Form unable to redirect after onSubmit Success

I would like to redirect to another page after a successful submit in redux-form.
I have tried the follow but the redirect either fails or doesn't do anything
REACT-ROUTER-DOM:
This results is an error 'TypeError: Cannot read property 'push' of undefined'
import { withRouter } from "react-router-dom";
FacilityComplianceEditForm = withRouter(connect(mapStateToProps (FacilityComplianceEditForm));
export default reduxForm({
form: "FacilityComplianceEditForm",
enableReinitialize: true,
keepDirtyOnReinitialize: true,
onSubmitSuccess: (result, dispatch, props) => {
props.history.push('/facilities') }
})(FacilityComplianceEditForm);
REACT-ROUTER-REDUX:
This submits successfully, the data is saved to the DB, but the page does not redirect.
import { push } from "react-router-redux";
export default reduxForm({
form: "FacilityComplianceEditForm",
enableReinitialize: true,
keepDirtyOnReinitialize: true,
onSubmitSuccess: (result, dispatch, props) => { dispatch(push('/facilities')) }
})(FacilityComplianceEditForm);
I also tried onSubmitSuccess: (result, dispatch, props) => dispatch(push('/facilities')) without the {} around dispatch statement but it didn't work
APP.JS to show the path does exist
class App extends Component {
render() {
return (
<div>
<Header />
<div className="container-fluid">
<Switch>
<Route exact path="/facilities" render={() => <FacilitySearch {...this.props} />} />
<Route exact path="/facilities/:id" render={(props) => <FacilityInfo id={props.match.params.id} {...this.props} />} />
<Route exact path="/facilities/compliance/:id" render={(props) => <FacilityComplianceEditForm id={props.match.params.id}{...this.props} />
<Redirect from="/" exact to="/facilities" />
<Redirect to="/not-found" />
</Switch>
</div>
<Footer />
</div>
);
}
}
export default App;
REDUCER:
export const complianceByIdReducer = (state = INTIAL_STATE.compId, action) => {
switch (action.type) {
console.log(state, action)
case "CREATE_NEW_COMPLIANCE":
return {
...state,
compId: action.compCreate
}
default:
return state
}
}
ACTION:
export const createCompliance = (id, compObj) => {
return dispatch => {
axios.post("/api/facilities/compliance/" + id, compObj)
.then(res => { return res.data })
.then(compCreate => {
dispatch(createComplianceSuccess(compCreate));
alert("New compliance created successfully") //this does get triggered
})
}
}
const createComplianceSuccess = compCreate => {
return {
type: "CREATE_NEW_COMPLIANCE",
compCreate: compCreate
}
}
REDIRECT OBJECT RETURNED FROM SUBMIT SUCCESS
STORE
import * as redux from "redux";
import thunk from "redux-thunk";
import {
facilityListReducer,
facilityReducer,
facilityLocationReducer,
facilityHistoricalNameReducer,
facilityComplianceReducer,
complianceByIdReducer
} from "../reducers/FacilityReducers";
import { projectFormsReducer } from "../reducers/FormsReducers";
import { errorReducer } from "../reducers/ErrorReducer";
import { reducer as formReducer } from "redux-form";
export const init = () => {
const reducer = redux.combineReducers({
facilityList: facilityListReducer,
facility: facilityReducer,
facilityLocation: facilityLocationReducer,
historicalNames: facilityHistoricalNameReducer,
facilityCompliance: facilityComplianceReducer,
compId: complianceByIdReducer,
countyList: projectFormsReducer,
errors: errorReducer,
form: formReducer
});
const store = redux.createStore(reducer, redux.applyMiddleware(thunk));
return store;
};
react-router-redux is deprecated so I did not want to add it to my project. I followed this post and made some modification to how routing was set up and it now works.

Use a custom render for rerender using react-testing-library

I'm using react-testing-library and I have a custom render:
const customRender = (node, ...options) => {
return render(
<ThemeProvider theme={Theme}>
<MemoryRouter>{node}</MemoryRouter>
</ThemeProvider>,
...options
);
};
I can use it successfully for a render in my test but not for a rerender:
test("shows content once data has loaded", () => {
const { queryByTestId, rerender, debug } = render(
<ConnectAccount
currentUser={{
loading: true
}}
/>
);
expect(queryByTestId("content")).toBeNull();
rerender(
<ConnectAccount
currentUser={{
user: {
name: "Test User"
}
}}
/>
);
debug();
});
I get an error TypeError: Cannot read property 'black' of undefined for the rerender. Is there any way for the rerender to use the custom render so I don't have to wrap every rerender in the ThemeProvider?
You need to redifine the rerender method. This should work:
const customRender = (node, options) => {
const rendered = render(
<ThemeProvider theme={Theme}>
<MemoryRouter>{node}</MemoryRouter>
</ThemeProvider>,
options);
return {
...rendered,
rerender: (ui, options) =>
customRender(ui, {container: rendered.container, ...options}),
}
};
Typescript version:
function AppProvider({ children }: { children: React.ReactNode }) {
return (
<OtherProvider>
<ThemeProvider theme={Theme}>
<MemoryRouter>{children}</MemoryRouter>
</ThemeProvider>,
</OtherProvider>
);
}
function renderInProvider(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(<AppProvider>{ui}</AppProvider>, options);
}

Currency field on Field ( redux-form ) for mobile app

I am currently developing a react mobile app.
The issue is that I want to add '£' currency symbol to a form-redux field with type=number.
Code
Form
import React from 'react';
import Card from "../../pandle-ui/Card";
import cssmodules from 'react-css-modules'
import styles from './form.cssmodule.scss'
import {Field} from "redux-form";
import CardTitleTextField from "../../form_fields/CardTitleTextField";
import {CardText, Divider, IconButton} from "material-ui";
import CardFieldRow from "../../layout/CardFieldRow";
import {TextFieldBase} from "../../form_fields/TextField";
import CurrencyField from "../../form_fields/CurrencyField";
import {CardActions, FlatButton} from "material-ui";
import DoneIcon from 'material-ui/svg-icons/action/done'
import {normalizePercentage} from "../../../util/currency_helpers";
class Form extends React.Component {
render() {
const {config, cancelButton, submit, wrapperRef} = this.props
return (
<div styleName="wrapper" ref={wrapperRef} >
<Card>
<DescriptionField />
<CardText style={ { paddingTop: 0 } }>
<QuantityField />
<UnitsField show={config.showUnits} />
<PriceField />
<DiscountField show={config.showDiscount} />
<Totals showTax={config.showTax} />
</CardText>
<Divider />
<CardActions style={{ textAlign: 'right', padding: '0 8px' }}>
{cancelButton}
<DoneButton onClick={() => submit()} />
</CardActions>
</Card>
</div>
)
}
}
Form.displayName = 'PagesLineItemsForm';
Form.propTypes = {};
Form.defaultProps = {};
export default cssmodules(Form, styles)
const DescriptionField = (props) =>
<Field
label="Description"
name="description"
component={CardTitleTextField}
{...props}
/>
const QuantityField = (props) =>
<CardFieldRow label="Quantity">
<Field name="quantity" type="number" component={TextFieldBase} {...props} />
</CardFieldRow>
const UnitsField = ({ show, ...props }) => {
if(show){
return <CardFieldRow label="Unit Type">
<Field name="unit" component={TextFieldBase} {...props} />
</CardFieldRow>
} else {
return null
}
}
const PriceField = (props) =>
<CardFieldRow label="Price">
<Field name="price" type="number" component={CurrencyField} {...props} />
</CardFieldRow>
const DiscountField = ({ show, props }) => {
if(show){
return <CardFieldRow label="Discount (%)">
<Field
name="discount_percentage"
component={TextFieldBase}
type="number"
normalize={normalizePercentage}
{...props}
/>
</CardFieldRow>
} else {
return null
}
}
const Totals = ({ showTax }) => {
if(showTax){
return <div>
<NetAmountField />
<TaxAmountField />
<TotalField />
</div>
} else {
return <TotalField name="net_amount" />
}
}
const NetAmountField = (props) =>
<CardFieldRow label="Net Amount">
<Field name="net_amount" component={CurrencyField} {...props} />
</CardFieldRow>
const TaxAmountField = (props) => {
return <CardFieldRow label="Tax Amount">
<Field name="tax_amount" component={CurrencyField} {...props} />
</CardFieldRow>;
}
const TotalField = (props) =>
<CardFieldRow label="Total">
<Field name="total_amount" disabled={true} component={CurrencyField} {...props} />
</CardFieldRow>
const DoneButton = (props) => <IconButton {...props}><DoneIcon /></IconButton>
CurrencyField.js
import React from 'react'
import {TextFieldBase} from "./TextField";
import {formatCurrency} from "../../util/formatters";
import {truncateCurrency} from "../../util/currency_helpers";
import {createComponentLogger} from "../../util/logging";
export default class CurrencyField extends React.Component {
constructor(props){
super(props)
log('Constructor called with props', props)
this.state = { value: props.value, focused : false }
this.baseInputPassProps = this.getBaseInputPassProps()
}
getBaseInputPassProps(){
return {
onChange: (e) => this.onChange(truncateCurrency(e.target.value)),
onBlur: (e) => this.onBlur(truncateCurrency(e.target.value)),
onFocus: (e) => this.onFocus()
}
}
onChange(value){
log('onChange called with', value)
safeParse(value, parsed => {
if(log('should update state', this.shouldUpdateStateValue(value))){
this.setState({ value })
this.props.input.onChange(parsed*100)
}
})
}
onBlur(value){
log('onBlur called with', value)
safeParse(value, parsed => {
this.props.input.onBlur(parsed*100)
})
}
onFocus(){
log('onFocus called')
this.props.input.onFocus()
}
shouldUpdateStateValue(value){
return this.state.value !== value
}
componentWillReceiveProps(nextProps){
const {value} = nextProps.input
log('will receive props', nextProps)
if(log('should update props value', this.shouldUpdatePropsValue(value))){
this.setState({
value: log('updating state.value', (value/100).toString())
})
}
}
shouldUpdatePropsValue(value){
const {input, meta: {active}} = this.props
return !active && input.value !== value
}
render(){
log('Render called')
return <TextFieldBase
{...log('Props for TextField', this.getTextFieldProps())}
/>
}
getTextFieldProps(){
const {value, ...props} = this.props
return Object.assign({}, props, this.getPassProps())
}
getPassProps(){
return { input: log('Input pass props', this.getInputPassProps()) }
}
getInputPassProps(){
return Object.assign({}, this.props.input, this.baseInputPassProps, {
value: log('Value for input pass props', this.getValue())
})
}
getValue(){
if(this.props.meta.active){
return log('active, state', this.state.value)
} else if(this.props.input.value) {
return log('inactive, formatted', this.getFormattedValue())
} else {
return ''
}
}
getFormattedValue() {
return (this.props.input.name === 'price') ?
this.props.input.value / 100
:
formatCurrency(
null,
log('for currency formatter', this.props.input.value / 100)
)
}
}
function safeParse(value, fn){
const parsed = Number(value)
log(`parsed ${value} as `, parsed)
!isNaN(parsed) && fn(parsed)
}
CurrencyField.displayName = 'CurrencyField'
const log = createComponentLogger(CurrencyField.displayName)
formatters.js
import numeral from 'numeral'
import 'numeral/locales/en-gb'
import moment from 'moment'
numeral.locale('en-gb');
export function formatCurrencyPence(number){
return (number > 0) ? formatCurrency(null, number/100) : '-'
}
export function formatCurrency(symbol='£', number){
return numeral(number).format('($0,0.00)')
}
export function formatAmount(number){
return numeral(number).format('-0,0.00')
}
export function formatInvoiceProps(item){
return [
formatInvoiceDates,
formatInvoiceAmounts
].reduce((memo, fn) => fn(memo), item)
}
const formatInvoiceDates = createFormatter(
formatServerDate,
'date',
'date-due'
)
function formatServerDate(date){
return formatMomentDate(serverToMomentDate(date))
}
function serverToMomentDate(date){
return moment(date, 'YYYY-MM-DD')
}
/**
* #param date {moment}
*/
export function formatMomentDate(date){
return (date.isValid() && date.format('DD/MM/YYYY')) || '-'
}
export function formatDate(date){
return formatMomentDate(moment(date))
}
const formatInvoiceAmounts = createFormatter(
formatInvoiceAmount,
'net-amount',
'tax-amount',
'total-amount'
)
function formatInvoiceAmount(amount){
return formatCurrency(null, amount)
}
function createFormatter(formatter, ...keys){
return ob => formatProps(ob, formatter, keys)
}
function formatProps(ob, formatter, keys){
return keys.reduce(formatProp(formatter), ob)
}
function formatProp(formatter){
return (ob, k) => {
const v = ob[k]
return v ? Object.assign({}, ob, { [k] : formatter(v, ob) }) : ob
}
}
This code uses number pad when the user tap in the field which is fine, but how can I format that?
Also, how can I prevent users to insert negative numbers?
Thx in advance.