Generic method to render React component depends on two props - javascript

I've got a component that gets two props, function and node(or string with label text), depends on these props I render the icon with some label. In future, I'm going to add more button and want to create the generic method that rendered this icon more flexible. So how to create such generic method for that?
const Wrapper = ({onRefresh, onExportToExcel, actionsLabel}) => {
return
{onRefresh && !!actionsLabel.refresh &&
<InlineIconButton name='refresh' label={actionsLabel.refresh} onClick={onRefresh} icon={<Autorenew/>} aria-label="Refresh"/>}
{onExportToExcel && !!actionsLabel.exportToExcel &&
<InlineIconButton name='exportToExcel' label={actionsLabel.exportToExcel} onClick={onExportToExcel} icon={<FileDownload/>} aria-label="ExportToExcel"/>}
}
<Wrapper onRefresh={()=> {}} onExportToExcel ={()=> {}} actionLabel={refresh: 'refresh', exportToExcel: 'export'}>

Maybe do something like:
const EXPORT_EXCEL = {
key: "EXPORT_EXCEL",
label: "export",
ariaLabel: "Export Excel",
icon: <Autorenew/>,
handler: params => { /* your function */ }
};
const REFRESH = {
key: "REFRESH",
label: "refresh",
ariaLabel: "Refresh",
icon: <FileDownload/>,
handler: params => { /* your function */ }
};
<Wrapper type={EXPORT_EXCEL} />;
const Wrapper = ({ type }) => {
return <InlineIconButton name={type.key} label={type.label} onClick={type.handler} icon={type.icon} aria-label={type.ariaLabel ? type.ariaLabel : type.label} />;
}
}
You even the possiblity to throw those EXPORT_EXCEL and REFRESH into array. Instead of having them loose put them in an array like so:
const BUTTONS = [
{
key: "EXPORT_EXCEL",
label: "export",
ariaLabel: "Export Excel",
icon: <Autorenew/>,
handler: params => { /* your function */ }
},
{
key: "REFRESH",
label: "refresh",
ariaLabel: "Refresh",
icon: <FileDownload/>,
handler: params => { /* your function */ }
},
];
And then loop through to create the Wrapper.
But then it's really up to you and your preferences and app's requirements

The entire idea behind React is to be able to create a unique component for every kind of usage. That is the entire philosophy behind React composability. Don't understand why would you want to wrap it.

Related

React update array prop from child to parent

Hello I am struggling to properly update my state from my child component to my parent.
Basically I am trying to set the current state to true onclick.
This is my parent component:
export default function Layout({ children }: Props) {
const [navigation, setNavigation] = useState([
{ name: 'Dashboard', href: '/', icon: HomeIcon, current: true },
{ name: 'Create Fact', href: '/facts/create', icon: UsersIcon, current: false },
{ name: 'Documents', href: '/documents', icon: InboxIcon, current: false }
])
return (
<>
<Sidebar navigation={navigation} setNavigation={setNavigation} />
This is my child Component (Sidebar)
type Props = {
navigation: Array<{
name: string
href: string
icon: any
current: boolean
}>
setNavigation: (
navigation: Array<{
name: string
href: string
icon: any
current: boolean
}>
) => void
}
const Sidebar = ({navigation, setNavigation}: Props) => {
const router = useRouter()
const toggleNavigation = (name: string) => {
// todo: Here I would like to properly update the state with the current selected navigation item (current)
const newNavigation = navigation.map(nav => {
if (nav.name === name) {
nav.current = true
return nav
}
})
}
return (
<nav className="flex-1 px-2 pb-4 space-y-1">
{navigation.map(item => (
<span
onClick={() => toggleNavigation(item.name)}
There are three problems:
You never call setNavigation with your new array.
You don't clear current on the formerly-current item.
Although you're creating a new array, you're reusing the objects within it, even when you change them, which is against the Do Not Modify State Directly rule.
To fix all three (see *** comments):
const toggleNavigation = (name: string) => {
const newNavigation = navigation.map(nav => {
if (nav.name === name) {
// *** #3 Create a *new* object with the updated state
nav = {...nav, current: true};
} else if (nav.current) { // *** #2 make the old current no longer current
nav = {...nav, current: false};
}
return nav;
});
// *** #1 Do the call to set the navigation
setNavigation(newNavigation);
};
Separately, though, I would suggest separating navigation out into two things:
The set of navigation objects.
The name of the current navigation item.
Then setting the navigation item is just setting a new string, not creating a whole new array with an updated object in it.
T.J. Crowder's solution and explanation are great.
Additionally, you can write that logic in a shorter syntax. Just a preference.
const newNavigation = navigation.map(nav => {
return nav.name === name
? { ...nav, current: true }
: { ...nav, current: false }
})

React component loads data twice on moving back-and-forth between tabs

For some reason my React component seems to remember its old state when going to another tab and back again, instead of reloading completely.
Basically when I click on the "Create" tab in my navbar and back to the "Board" tab data is populated twice instead of once, see image below. When going back the Board component this.state has two of each taskIds, as if it the component state still had the data from the initial page load when loading again. I have a React component looking like this:
const columnOrder = ['todo', 'in-progress', 'in-review', 'done']
const EMPTY_COLUMNS = {
'todo': {
id: 'todo',
title: 'TODO',
taskIds: []
},
'in-progress': {
id: 'in-progress',
title: 'In Progress',
taskIds: [],
},
'in-review': {
id: 'in-review',
title: 'In Review',
taskIds: []
},
'done': {
id: 'done',
title: 'Done',
taskIds: []
}
};
export class Board extends Component {
constructor(props) {
super(props);
this.onLoadEpic = this.onLoadEpic.bind(this);
this.state = {
columnOrder: columnOrder,
columns: {
'in-progress': {
id: 'in-progress',
title: 'In Progress',
taskIds: [],
},
// ...more columns similar to above
},
};
// Load state data on mount
componentDidMount() {
loadEpic(arg1, arg2);
}
// Async function loading items from DB and formatting into useful columns
async loadEpic(arg1, arg2) {
axios.get(...)
.then((response) => {
let data = response.data;
let newTasks = {};
let newColumns = EMPTY_COLUMNS;
data.taskItems.forEach(function(item) {
let id = item.id.toString();
newColumns[item.status]["taskIds"].push(id);
newTasks[id] = {
...item,
id: id
}
});
this.setState({
tasks: newTasks,
columns: newColumns
});
})
}
render() {
// Prints ["7"] on initial load and ["7", "7"] after going back and forth
console.log(this.state.columns["in-progress"].taskIds);
return (
// Simplified, but this is the main idea
<Container>
<DragDropContext onDragEnd={this.onDragEnd}>
{
this.state.columnOrder.map((columnId) => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId]
return (
<Column key={column.id} column={column} tasks={tasks}/>
)
}
}
</DragDropContext>
</Container>
)
}
}
and an App.js with Routing looking like this:
export default class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={Board} />
<Route exact path='/create' component={Create} />
</Layout>
);
}
}
Okay, so I figured it out: it's the EMPTY_COLUMNS constant that is bugging out. When the component is re-rendered, the same EMPTY_COLUMNS object is referenced - so the constant is being appended to. Instead, I should make a copy of the empty columns:
// Before - same object is being appended to, doesn't work
let newColumns = EMPTY_COLUMNS;
// After - create a deep copy of the constant, does work
let newColumns = JSON.parse(JSON.stringify(EMPTY_COLUMNS));

Rendering the navigation list from an array based on different label on toggle mode

I have a header component where I need to render three buttons, so every three buttons have three props. One is the class name, click handler and text.
So out of three buttons, two buttons act as a toggle button, so based on the click the text should change.
See the below code:
class App extends Component(){
state = {
navigationList: [{
text: 'Signout',
onClickHandler: this.signoutHandler,
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode,
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay,
customClassName: 'buttonStyle'
}]
}
signoutHandler = () => {
// some functionality
}
viewMode = () => {
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay = () => {
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<Header navigationList={this.state.navigationList}/>
)
}
}
const Header = ({navigationList}) => {
return (
<>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</>
)
}
The other way is I can pass all the props one by one and instead of an array I can write three button elements render it, but I am thinking to have an array and render using a map.
So which method is better, the problem that I am facing is if use the array. map render
the approach I need to set the initial value as a variable outside and how can I set the state.
And I am getting the onClick method is undefined, is it because the function is not attached to the state navigation list array.
Update
I declared the functions above the state so it was able to call the function.
So in JS, before the state is declared in the memory the functions should be hoisted isn't.
class App extends React.Component {
constructor(props){
super();
this.state = {
isStudents:false,
activeWay:false,
}
}
createList(){
return [{
text: 'Signout',
onClickHandler: this.signoutHandler.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay.bind(this),
customClassName: 'buttonStyle'
}];
}
signoutHandler(){
}
viewMode(){
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay(){
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<div>
<div>ddd</div>
<Header navigationList={this.createList()} />
</div>
)
}
}
const Header = ({navigationList}) => {
console.log(navigationList);
return (
<div>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
https://jsfiddle.net/luk17/en9h1bpr/
Ok I will try to explain, If you see you are using function expressions in your class and as far as hoisting is concerned in JavaScript, functions expressions are not hoisted in JS only function declarations are hoisted, function expressions are treated as variables in JS.
Now for your case you don't have to shift your functions above the state, you can simply use constructor for initializing state as
constructor(props) {
super(props);
this.state = {
isStudents: false,
activeWay: false,
navigationList: [
{
text: "Signout",
onClickHandler: this.signoutHandler,
customClassName: "buttonStyle"
},
{
text: "Teachers",
onClickHandler: this.viewMode,
customClassName: "buttonStyle"
},
{
text: "Active Hidden",
onClickHandler: this.activeWay,
customClassName: "buttonStyle"
}
]
};
}
Now you will have your handlers available as it is
Sandbox with some modification just to show
EDIT:
You can have default text for buttons and change it when clicking,
Sandbox updated
Hope it helps

Label text not updating in MUIDataTable ReactJS

I want to add multi language option in mui Datatables. I can change the translations but when I want to change language, I tried to give another object with the other translations (this object if I do console log I can see the changes) but the label texts not change.
I used a contextProvider to change the language selected and then get the specific dictionary with the translations.
Is a class component, so I did a static contextType with the correct provider.
Is there any possibility to re-render the element with another options or something like that?
options = {
textLabels: this.context.translation.dataTables.textLabels
};
return(
<MUIDataTable
title={this.context.language.value}
data={data}
columns={columns}
options={options}
/>
);
The best approach to re-render Mui-Datatables its updating the key of the table
key={this.context.language.value}
<MUIDataTable
key={this.context.language.value}
title={this.context.language.value}
data={data}
columns={columns}
options={options}
/>
You can force React component rendering:
There are multiple ways to force a React component rendering but they are essentially the same. The first is using this.forceUpdate(), which skips shouldComponentUpdate:
someMethod() {
// Force rendering without state change...
this.forceUpdate();
}
Assuming your component has a state, you could also call the following:
someMethod() {
// Force rendering with a simulated state change
this.setState({ state: this.state });
}
use customRowRender Function in the options and manipulate table with respect to language
Override default row rendering with custom function.
customRowRender(data, dataIndex, rowIndex) => React Component
In MUIDataTable, We can override label name by providing label in MUIDataTableColumnDef options while making column.
Example :
const columns: MUIDataTableColumnDef[] = [
{
name: 'Id',
label: 'ID',
options: {
download: false,
customBodyRenderLite: (index: number) => {
const desc: Description = evenMoreAbout[index]
return <BasicInfo obj={desc} setIconClicked={setIconClicked} />
}
}
},
{
name: 'id',
label: 'ID',
options: {
display: 'excluded',
download: true,
customBodyRender: desc => desc.id
}
}]
Even though if we still want to over ride the label name on some condition of data using customHeadLabelRender ... we can as like below example
const columns: MUIDataTableColumnDef[] = [
{
name: 'Id',
label: '',
options: {
download: false,
customBodyRenderLite: (index: number) => {
const desc: Description = evenMoreAbout[index]
return <BasicInfo obj={desc} setIconClicked={setIconClicked} />
},
customHeadLabelRender: (dataIndex: number, rowIndex: number) => {
return 'ID';
}
}
}
]

Mobx react form add fields on click depending on condition

I have a function that generates fields for a form like so:
export const makeFields: Function = (itemData: Object) => {
return [
{
// PROJECT DETAIL SECTION
name: 'chooseAccount',
label: 'Choose Account',
fields: [{
name: 'account',
label: 'Choose Trading Account',
rules: 'required',
...(itemData ? { value: itemData.trading_account ? itemData.trading_account.name : null } : null)
}]
},
{
name: 'projectDetails',
label: 'Project detail',
fields: [
{
name: 'projectCode',
label: 'Project code',
rules: 'required',
...(itemData ? { value: itemData.code } : null)
},
...
and the component that uses this function for the form fields:
...
export default class ProjectForm extends React.Component<*> {
props: TypeProps;
getMode = () => this.props.mode
componentDidMount() {
const projectDetailsStore: Object = this.props.projectDetailsStore;
this.getMode() === 'edit'
?
projectDetailsStore.loadProjectDetails(this.props.projectId)
:
projectDetailsStore.resetStore();
}
#computed get form(): Object {
const itemData: Object = (typeof this.props.itemData === 'undefined') ? {} : this.props.itemData;
const fields: Array<*> = makeFields(this.props.projectDetailsStore.details);
return this.getMode() === 'edit'
? projectEdit(fields, itemData)
: projectCreate(fields);
}
render(): React.Element<*> {
const t: Function = this.props.t;
const TAmodel: AutoCompleteData = new AutoCompleteData(autoCompleteTradingAccounts);
const Pmodel: AutoCompleteData = new AutoCompleteData(autoCompleteProject);
const projectDetailsStore: Object = this.props.projectDetailsStore;
this.form.add(
{
name: 'test'
}
)
console.log(this.form)
return (
<PageWrapper>
{projectDetailsStore.loadingProjectDetails
?
<Loader />
:
<FormWrapper form={this.form}>
<form>
<FormSection form={this.form} section="chooseAccount">
<InputLabel htmlFor="account">
{t('projectForm: Choose trading account')}
</InputLabel>
<ElectroTextField field={this.form.$('chooseAccount.account')} />
{/* <ElectroAutoComplete
field={this.form.$('chooseAccount.account')}
form={this.form}
props={{
model: TAmodel
}}
/> */}
</FormSection>
<FormSection form={this.form} section="projectDetails">
<ElectroTextField field={this.form.$('projectDetails.projectCode')} />
<ElectroTextField field={this.form.$('projectDetails.projectName')} />
...
I would like to add some fields to the form based on a condition. I have tried the following:
this.form.add(
{
name: 'test'
}
)
it doesn't throw an error but nothing happens.
The add method takes an object as per the docs (https://foxhound87.github.io/mobx-react-form/docs/api-reference/fields-methods.html). Ideally I would like a click event to fire the add method and add the newly created field.
this.form is neither a React state nor a MobX observable, so that nothing happens when you change its value.
To get it to work, you should create an observable form field that is initialized by makeFields, and use an action function to change its value, and then use observer to re-render.
If you are not quite familiar with mentioned above, read React & MobX official tutorials first.

Categories