How do I create react-router v4 breadcrumbs? - react-router-v4

How do I create react-router v4 breadcrumbs? I tried asking this question on the react-router V4 website via an issue ticket. They just said to see the recursive paths example. I really want to create it in semantic-ui-react

I was after the same thing and your question pointed me in the right direction.
This worked for me:
const Breadcrumbs = (props) => (
<div className="breadcrumbs">
<ul className='container'>
<Route path='/:path' component={BreadcrumbsItem} />
</ul>
</div>
)
const BreadcrumbsItem = ({ match, ...rest }) => (
<React.Fragment>
<li className={match.isExact ? 'breadcrumb-active' : undefined}>
<Link to={match.url || ''}>
{match.url}
</Link>
</li>
<Route path={`${match.url}/:path`} component={BreadcrumbsItem} />
</React.Fragment>
)

I used semantic-ui-react for my own project and did this to create breadcrumbs based on location.pathname;
export default (props) => {
const paths = props.pathname.split('/').map((p, i, arr) => {
if (i === 0) return {
key: i,
content: (<Link to={'/'}>home</Link>),
active: (i === arr.length - 1),
link: (i < arr.length - 1)
};
if (i === arr.length - 1) return {
key: i,
content: p,
active: (i === arr.length - 1)
};
return {
key: i,
content: (<Link to={`${arr.slice(0, i + 1).join('/')}`}>{p}</Link>),
active: (i === arr.length - 1),
link: (i < arr.length - 1)}
};
);
return <Breadcrumb icon='chevron right' sections={paths}/>;
};

The problem with both of these approaches is that you're limited to using the path name in the breadcrumb trail; that is, you have to tie your routing to the presentation names of your trail.

This can be done using a HOC that parses the pathname from react-router and returns matches against it. While a little more verbose, I think it gives greater flexibility and a nice readable breadcrumb config object array.
Breadcrumbs.jsx
import React from 'react';
import { NavLink } from 'react-router-dom';
import { withBreadcrumbs } from 'withBreadcrumbs';
const UserBreadcrumb = ({ match }) =>
<span>{match.params.userId}</span>; // use match param userId to fetch/display user name
const routes = [
{ path: 'users', breadcrumb: 'Users' },
{ path: 'users/:userId', breadcrumb: UserBreadcrumb},
{ path: 'something-else', breadcrumb: ':)' },
];
const Breadcrumbs = ({ breadcrumbs }) => (
<div>
{breadcrumbs.map(({ breadcrumb, path, match }) => (
<span key={path}>
<NavLink to={match.url}> // wrap breadcrumb with semantic-ui element
{breadcrumb}
</NavLink>
<span>/</span>
</span>
))}
</div>
);
export default withBreadcrumbs(routes)(Breadcrumbs);
withBreadcrumbs.js
import React from 'react';
import { matchPath, withRouter } from 'react-router';
const renderer = ({ breadcrumb, match }) => {
if (typeof breadcrumb === 'function') { return breadcrumb({ match }); }
return breadcrumb;
};
export const getBreadcrumbs = ({ routes, pathname }) => {
const matches = [];
pathname
.replace(/\/$/, '')
.split('/')
.reduce((previous, current) => {
const pathSection = `${previous}/${current}`;
let breadcrumbMatch;
routes.some(({ breadcrumb, path }) => {
const match = matchPath(pathSection, { exact: true, path });
if (match) {
breadcrumbMatch = {
breadcrumb: renderer({ breadcrumb, match }),
path,
match,
};
return true;
}
return false;
});
if (breadcrumbMatch) {
matches.push(breadcrumbMatch);
}
return pathSection;
});
return matches;
};
export const withBreadcrumbs = routes => Component => withRouter(props => (
<Component
{...props}
breadcrumbs={
getBreadcrumbs({
pathname: props.location.pathname,
routes,
})
}
/>
));
Open-source HOC is also available here:
https://github.com/icd2k3/react-router-breadcrumbs-hoc

Try this simple solution:
const Breadcrumbs = ({ ...rest, match }) => (
<span>
<Link to={match.url || ''} className={match.isExact ? 'breadcrumb active' : 'breadcrumb'}>
{match.url.substr(match.url.lastIndexOf('/')+1, match.url.length)}
</Link>
<Route path={`${match.url}/:path`} component={Breadcrumbs} />
</span>
)
Your css:
.breadcrumbs {
background: #fff;
margin-bottom: 15px;
}
.breadcrumb {
margin-bottom: 0;
line-height: 2.5;
display: inline-block;
}
.breadcrumb::before {
display: inline-block;
padding-right: 5px;
padding-left: 5px;
color: #818a91;
content: "/"; }
.breadcrumb.active {
color: #818a91; }
Then use it like this:
<div className="container-fluid breadcrumbs">
<Route path='/:path' component={Breadcrumbs} />
</div>
Happy coding!

Here is the solution providing single source of truth for nested navigation and breadcrumbs.
The example app is available on GitHub: https://github.com/sneas/react-nested-routes-example
Demo: https://sneas.github.io/react-nested-routes-example/
Navigation configuration:
export const navigation = [
{
path: "/",
label: "All categories",
content: () => <AllCategories />,
routes: [
{
path: "/electronics",
label: "Electronics",
content: () => <Electronics />,
routes: [
{
path: "/accessories",
label: "Accessories",
content: () => <Accessories />,
routes: [
{
path: "/usb-cables",
label: "USB cables",
content: () => <UsbCables />
}
]
},
{
path: "/headphones",
label: "Headphones",
content: () => <Headphones />
}
]
}
]
}
];
We have to recursively flatten navigation and render it a flat array:
const routes = flattenRoutes(navigation);
return (<Router>
{routes.map((route, index) => (
<Route
key={index}
path={route.path}
render={() => rouete.content}
></Route>
))}
</Router>);
Then build breadcrumbs out of the same navigation structure.

From react-router doc: the <Route> component is rendering some of your components when a location matches the route’s path like this:
<Route path={`${match.url}/:topicId`} component={Topic}/>
Basic responsibility to the information is available to the rendered component which is <Topic> in this case. It knows how to fetch data or it already has tied Redux state and so on. So <Topic> simply instantiates the breadcrumbs item agent and passes the required data to it like this:
import {BreadcrumbsItem} from 'react-breadcrumbs-dynamic'
const Topic = ({ match, topic }) => (
<div>
<h3>
{topic.title}
</h3>
<BreadcrumbsItem to={`${match.url}/${match.params.topicId}`}>
{topic.title}
</BreadcrumbsItem>
...
</div>
)
That's all. A fuller example in this answer. Here is the live demo.

Related

Unable to find node on an unmounted component (react-sortable-hoc)

A have small gutenberg block, but i have errors.
Unable to find node on an unmounted component
undefined is not an object (evaluating 'this.refs[collection].indexOf')
Current wp version uses React 16.13.1
How can i fix this? Thanks!
import {SortableContainer, SortableElement} from 'react-sortable-hoc';
registerBlockType('ay/sortable', {
title: __('Sortable'),
attributes: {
items: {
type: 'array',
default: [
'Item 1',
'Item 2',
'Item 3',
],
},
},
edit(props) {
const {
attributes: {items},
setAttributes,
} = props;
const SortableItem = SortableElement(({value}) => <li>{value}</li>);
const SortableList = SortableContainer(({items}) => {
return (
<ul>
{items.map((value, index) => (
<SortableItem
key={`item-${value}`}
index={index}
value={value}
/>
))}
</ul>
);
});
const onSortEnd = ({oldIndex, newIndex}) => {
setAttributes(({items}) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
return (
<div>
<SortableList items={items} onSortEnd={onSortEnd} />
</div>
);
},
save(props) {
return null;
},
});
In your code provided, there are two critical imports missing for your block to work, add:
...
import { registerBlockType } from '#wordpress/blocks';
import { __ } from '#wordpress/i18n';
...
Also check that your package.json lists react-sortable-hoc as a dependancy:
...
"devDependencies": {
...
react-sortable-hoc": "^2.0.0"
},
After adding the missing imports and making sure the dependancy is present/installed, your block should compile without error. I tested your code all the other parts of the edit() function are fine and the output was a sortable list.

PDFtron & react Error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer

I am trying to show in the app that I built in React a PDF file using PDFtron and encounter the following error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer.
my code is:
import { url } from "../../../../config.json";
import React, { useState, useEffect, useRef } from "react";
import { getProject } from "../../../../services/projectService";
import { useParams } from "react-router-dom";
import WebViewer from "#pdftron/webviewer";
import { getCurrentUser } from "../../../../services/userService";
import { Link, Redirect } from "react-router-dom";
import { deleteImage } from "../../../../services/projectService";
const MyContracts = () => {
const [project, setProject] = useState({});
const [counter, setCounter] = useState(0);
const [files, setFiles] = useState([]);
const { id } = useParams();
// const viewerDiv = useRef();
const user = getCurrentUser();
const [viewerUrl, setViewerUrl] = useState(`${url}/files/testing.pdf`);
const viewer = document.getElementById("viewer");
useEffect(() => {
getProject(id)
.then(res => {
setProject(res.data);
setFiles(res.data.files.contracts);
})
.catch(error => console.log(error.message));
}, []);
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
if (!user) return <Redirect to="/private-area/sign-in" />;
if (user && user.isAdmin | (user._id === project.userID))
return (
<div className="container">
</div>
{/********** PDF VIEWER ************/}
<div className="web-viewer" id="viewer"></div>
{/* <div className="web-viewer" ref={viewerDiv} id="viewer"></div> */}
{/********** PDF Gallery ************/}
{files !== undefined && (
<>
<h2 className="text-rtl h3Title mt-2">בחר קובץ</h2>
<select
id="select"
className="col-12 text-rtl px-0"
onChange={e => setViewerUrl(e.target.value)}>
{files.map((file, index) => (
<option value={`${url}${file.url}`} key={index}>
{file.name}
</option>
))}
</select>
</>
)}
</div>
);
};
export default MyContracts;
What am I doing wrong and how can I fix it?
I see that you are trying to load multiple instances of WebViewer:
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
Webviewer cannot be instantiated more than once in the same HTML element. If you need a completely different instance, you can hide or remove the HTML element and create a new one to hold the new instance.
That being said, if you just need to load another document, I would recommend using the loadDocument API. You can read more about it here as well.
​

React Native Redux - Limiting component rerenders caused when redux state changes (for array children)

I am passing redux state into my component using the following
const mapStateToProps = state => ({
rdx_myColorPreference: state.profile.mySelf.preferences[4],
});
My goal is that the component only rerenders when the 5th child in the preferences array is changed (i.e. preferences[4]....which relates to color preferences).
Problem is that when ANY child in the preferences array changes (e.g. preferences[0]....which relates to food preferences), the component rerenders.
Each child should be a pure component (use React.memo) and when you pass a function from the parent then make sure you don't pass a newly created function each time the parent renders so you need to do onChange={function} and not onChange={()=>newly created function}.
Here is an example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { memo, useCallback } = React;
const initialState = {
items: [
{ id: 1, value: '' },
{ id: 2, value: '' },
{ id: 3, value: '' },
],
};
//action types
const CHANGE_ITEM = 'CHANGE_ITEM';
//action creators
const changeItem = (id, value) => ({
type: CHANGE_ITEM,
payload: { id, value },
});
const reducer = (state, { type, payload }) => {
if (type === CHANGE_ITEM) {
const { id, value } = payload;
return {
...state,
items: state.items.map((item) =>
item.id !== id ? item : { ...item, value }
),
};
}
return state;
};
//selectors
const selectItems = (state) => state.items;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
//use React.memo to create a pure component
const Item = memo(function Item({
item: { id, value },
onChange,
}) {
console.log('render item wih id:', id);
return (
<ul>
{id}
<input
type="text"
value={value}
onChange={(e) => onChange(id, e.target.value)}
/>
</ul>
);
});
const App = () => {
const items = useSelector(selectItems);
const dispatch = useDispatch();
//create this onChange function on mount using React.useCallback
const onChange = useCallback(
(id, value) => dispatch(changeItem(id, value)),
//dependencies is dispatch but that will never change so it is
// only created on mount
[dispatch]
);
return (
<ul>
{items.map((item) => (
<Item
key={item.id}
item={item}
onChange={onChange}
/>
))}
</ul>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

React Native - How to display the data from an array items stored with AsyncStorage

I working on a simple tasks app (To do App).
Currently the process of creating a new object with unique ID for each task (text typed in the input) is working correctly,
but I am not sure how to simply display the information 'text' from inside each object in a list object.
I created a List object to display but I think I'm using the wrong way or data to display what I want.
export default class Main extends Component {
state = {
newUser: '',
users: ['wagner'],
};
handleAddUser = () => {
//console.tron.log(this.state.newUser);
const newUser = this.state.newUser;
const { users } = this.state;
//console.tron.log(newUser);
// Check newUser (input) and create a new object
if (newUser !== '') {
this.setState(prevState => {
const id = uuid();
const newItemObject = {
[id]: {
id,
isCompleted: false,
text: newUser,
createdAt: Date.now(),
},
};
console.tron.log(newItemObject);
console.tron.log(newItemObject[id].text);
// store the new state on the array
const newState = {
...prevState,
newUser: '',
users: {
...prevState.users,
...newItemObject,
},
};
this.saveItems(newState.users);
return { ...newState };
console.tron.log(users);
});
}
};
saveItems = newItem => {
const saveItem = AsyncStorage.setItem('To Dos', JSON.stringify(newItem));
};
//console.tron.log(this.state.users);
render() {
const { users, newUser, newState } = this.state;
const { navigate } = this.props.navigation;
return (
<Container>
<Form>
<Input
placeholder="Add a new task"
value={newUser}
onChangeText={text => this.setState({ newUser: text })}
onSubmitEditing={this.handleAddUser}
/>
<SubmitButton onPress={this.handleAddUser}>
<Icon name="add" size={20} color="#eee" />
</SubmitButton>
</Form>
<List
data={users}
keyExtractor={user => user.id}
renderItem={({ item }) => (
<Task>
{item.text}
<Text>Test</Text>
</Task>
)}
/>
</Container>
);
}
}
My styled file where List and Task are built:
export const List = styled.FlatList.attrs({
showVerticalScrollIndicator: false,
})`
margin-top: 20px;
`;
export const Task = styled.View`
align-items: center;
margin: 0 20px 30px;
`;

Required props for List and Edit components in route when using CustomApp

I'm trying to upgrade a custom app from admin-on-rest to react-admin (v2.15). After I figured out that the declareResources action was "replaced" by registerResource most seemed ok, but I still struggle with the List and Edit components route definitions that complains about missing required props (compared to what props are defined in the custom app documentation).
If I define a List component like this it works fine:
<Route exact path="/mystuffs" render={(routeProps) => <MystuffList hasCreate hasEdit hasShow={false} hasList resource="mystuffs" basePath="/mystuffs" {...routeProps} />} />
Similar the only way I can get an Edit-component to work is to pass the required props like so:
<Route exact path="/mystuffs/:id" render={(routeProps) => <MystuffEdit resource="mystuffs" id={routeProps.match.params.id} basePath="/mystuffs" {...routeProps} />} />
But to me it seems a bit tedious to define all of these props (i.e was not required with admin-on-rest). Is this the correct way of doing it or am I missing something obvious here since the custom app documentation doesn't specify all of the required props?
I ended up adding a custom resource component that is pretty similar to the Resource in react-admin. Something like this:
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Switch, Route } from 'react-router-dom';
const RACustomResource = ({ resource, list, edit, create, show, path, children, ...rest }) => {
const { computedMatch, ...options } = rest;
const listResource = list ?
(<Route
exact
path={path}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(list, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
...routeOpts,
...options,
});
}}
/>)
: null;
const createResource = create ?
(<Route
path={`${path}/create`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(create, {
basePath: path || routeProps.match.url,
resource,
hasList: !!list,
hasShow: !!show,
record: {},
...routeOpts,
...options,
});
}}
/>)
: null;
const editResource = edit ?
(<Route
exact
path={`${path}/:id`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(edit, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
id: routeProps.match.params.id,
...routeOpts,
...options,
});
}}
/>)
: null;
const showResource = show ?
(<Route
exact
path={`${path}/:id/show`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(show, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
id: routeProps.match.params.id,
...routeOpts,
...options,
});
}}
/>)
: null;
return (
<Switch>
{createResource}
{showResource}
{editResource}
{listResource}
{children}
</Switch>
);
};
RACustomResource.propTypes = {
resource: PropTypes.string.isRequired,
path: PropTypes.string,
basePath: PropTypes.string,
children: PropTypes.any,
list: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
create: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
edit: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
show: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
};
export default RACustomResource;
// used like this
// <RACustomResource path="/myresource" resource="myresource" list={MyResourceList} create={MyResourceCreate} edit={MyResourceEdit} />
It is indeed required. We have still a lot of work to do on the custom app side, including documentation.
You can help us! Can you explain why you needed to use react-admin this way? What wasn't possible using the default Admin? etc.
Thanks!