I have a component called Tools which is accessed when I am on the route tools/:id. On this page, I have buttons with Links to this page, but it passes in a different id to the URL when clicked so that a different tool will be shown.
The getTool method just returns the correct component, and the component will only have one thing, an iFrame to show a calculator from another website.
So when I go back and forth between tools, the tools aren't loading until I click refresh. Otherwise, I get an error that says TypeError: Cannot read property 'style' of null. This is because I have document.getElementById('calc-SOME-NUMBER') but SOME_NUMBER is still referring to the last tool I was on. And this statement is within each tool and you can this below in BCalc.
I've checked the state, and when I go back and forth between tools, everything is correct; the correct tool is placed in the reducer. Any idea why this is happening? I was using history.go() as a workaround because I can't see any reason it still is lingering on to the old tools id.
const Tool = ({
getContent,
closeContent,
content: { content, loading, contents },
user,
match,
}) => {
const history = useHistory();
useEffect(() => {
scrollToTop();
getContent(match.params.id);
return () => {
logView(user, match.params.id, "tool");
closeContent();
history.go(); // doesn't work without this line
};
}, [getContent, closeContent, match.params.id, user]);
let ToolComponent;
const listItemStyle = { paddingTop: "40px", paddingBottom: "40px" };
return loading || content === null ? (
<Spinner />
) : (
<Container
style={{ marginTop: "3%" }}
fluid="true"
className="contentCardBody"
>
<Card>
<Card.Body>
{(ToolComponent = getTool(content.content_id.path))}
<ToolComponent />
</Card.Body>
</Card>
</Container>
);
};
Tool.propTypes = {
content: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
getContent: PropTypes.func.isRequired,
closeContent: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
content: state.content,
user: state.auth.user,
});
export default connect(mapStateToProps, {
getContent,
closeContent,
})(Tool);
Also, here is an example of what is returned from getTool():
const BCalc = () => {
const eventMethod = window.addEventListener ? 'addEventListener' : 'attachEvent';
const eventer = window[eventMethod];
const messageEvent = eventMethod === 'attachEvent' ? 'onmessage' : 'message';
eventer(
messageEvent,
(e) => {
if (e.origin === 'https://EXAMPLE.com') {
if (e.data.startsWith('height;')) {
document.getElementById('calc-SOME_NUMBER').style.height = `${e.data.split('height;')[1]}px`;
} else if (e.data.startsWith('url;')) {
window.location.href = e.data.split('url;')[1];
}
}
},
!1,
);
return (
<div>
<iframe
id="calc-SOME_NUMBER"
src="https://EXAMPLE_CALC"
title="tool"
scrolling="no"
style={{
width: '100%',
border: 'none',
marginTop: '-2%',
zIndex: '-1',
}}
/>
</div>
);
};
export default BCalc;
Related
In my project, I actually use the context api. Additionally, I pass the name of an image from my component to the context provider on onclick, and then I take the name from the provider and pass it to another component. It is operating flawlessly. But there was a console error. When I send data to the context and access it in another component, the log always displays the same same props error.
TemplateLib:
<ImagesGrid
images={images}
getPreview={(image) => window.location.origin + '/' + image.image}
onSelect={async (image) => {
//here i'm passing the name to the provider function
await fetchEditData(image.name)
// localStorage.setItem('template_name', image.name)
const altered = JSON.parse(image.template_json)
store.loadJSON(altered);
}}
/>
Provider:
const App = ({ store }) => {
const [data, setData] = useState('')
const fetchEditData = async (name) => {
setData(await name)
}
return (
<PolotnoContext.Provider value={{ fetchEditData, data }} >
<div
style={{
width: '100%',
height: height + 'px',
display: 'flex',
flexDirection: 'column',
}}
>
// body
</div>
</PolotnoContext.Provider>
);
};
topBar.jsx:
// Here I'm getting the name
export default observer(({ store }) => {
const { data } = useContext(PolotnoContext)
console.log(data) // here same props error occur
return (
<>
<NavbarContainer className="bp4-navbar">
</NavbarContainer >
</>
);
});
i'm using the tab view react native library to build a user account screen. my request is simple, how can i update the tab view content after an api call that fetches the user data?
function UserStoreScreen({ navigation, route }) {
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(0);
const [userStore, setUserStore] = React.useState({});
const [routes] = React.useState([
{ key: "first", title: "Dressing" },
{ key: "second", title: "À propos" },
]);
const user = route.params;
// renders undefined
const FirstRoute = () => (
<>
<View style={styles.userContainer}>
<ListItem
image={`${IMAGES_BASE_URL}${userStore.photo}`}
title={`${userStore.username}`}
subTitle={`${userStore.store.length} Articles`}
/>
</View>
</>
);
const SecondRoute = () => (
<>
<View style={{ flex: 1, backgroundColor: "#ff4081" }} />
</>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
const getUser = async () => {
await axiosApi
.post("/getUserProducts", { user_id: user.user_id })
.then((response) => {
// didn't work since set state is async
setUserStore(response.data);
})
.catch((err) => {
console.log(err);
});
};
// Get store products
useEffect(() => {
getUser();
}, []);
return (
<Screen style={styles.screen}>
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
}
is there a way to make the content of the tab view updated after i receive the data from the api call?
Yes, there is a way to forcefully re-mount a component. To do that, we can use key props like this:
return (
<Screen style={styles.screen}>
<TabView
key={JSON.stringify(userStore)}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
How does key props work? Every time a component is re-rendering, it will check whether the key value is the same or not. If it's not the same, then force a component to re-render.
In this case we will always check if userStore value has changed or not.
I have a component that currently uses the useDrag hook to connect to react-dnd. It works well, except for previews. I want to implement useDragLayer instead to see if it would help with my preview problems, as many online threads suggest.
This is my current (simplified) useDrag implementation:
const [{ isDragging }, connectDragSource, connectPreview] = useDrag({
item,
collect: monitor => ({
isDragging: monitor.getItem()?.index === item.index,
})
})
return (
<Wrapper ref={connectPreview} isDragging={isDragging}>
<DragHandle ref={connectDragSource} />
</Wrapper>
)
How do I use useDragLayer in this context, in a way that might help with my previews? The docs example makes little sense to me...
How do I connect my rendered components using useDragLayer api? useDragLayer doesn't return drag source and preview connector functions (like useDrag does on index 1 and 2 of the returned array), and its collect function doesn't provide a DragSourceConnector instance either. So what do I do with the hook/returned value after I call it?
I just resolved this and want to share it to help others :)
You will need to do couple of things for this to fully work.
Disable the default preview behavior by adding the following useEffect
import { getEmptyImage } from "react-dnd-html5-backend";
const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
type: "BOX",
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
useEffect(() => {
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);
Create the custom default layer
export const CustomDragLayer = (props: {}) => {
const {
itemType,
isDragging,
initialCursorOffset,
initialFileOffset,
currentFileOffset,
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialCursorOffset: monitor.getInitialClientOffset(),
initialFileOffset: monitor.getInitialSourceClientOffset(),
currentFileOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div
style={getItemStyles(
initialCursorOffset,
initialFileOffset,
currentFileOffset
)}
>
<div>Your custom drag preview component logic here</div>
</div>
</div>
);
};
const layerStyles: CSSProperties = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0,
top: 0,
width: "100%",
height: "100%",
border: "10px solid red",
};
function getItemStyles(
initialCursorOffset: XYCoord | null,
initialOffset: XYCoord | null,
currentOffset: XYCoord | null
) {
if (!initialOffset || !currentOffset || !initialCursorOffset) {
return {
display: "none",
};
}
const x = initialCursorOffset?.x + (currentOffset.x - initialOffset.x);
const y = initialCursorOffset?.y + (currentOffset.y - initialOffset.y);
const transform = `translate(${x}px, ${y}px)`;
return {
transform,
WebkitTransform: transform,
background: "red",
width: "200px",
};
}
Add the <CustomDragLayer /> to the top-level component
You will need to include the ref={drag} to the component you want to drag and remove the connectPreview ref completely.
Hopefully, this helps you.
I have a Drawer Navigator set up, with various screens.
On one screen, I have a FlatList with a simple TouchableOpacity like below:
<TouchableOpacity
onPress={() =>
navigation.navigate("ViewProject", {
screen: "viewProject",
params: { projectID: item.projectID },
})
}
>
</TouchableOpacity>
The navigation works, going to the viewProject screen, but no matter what I seem to try the route.params just doesn't seem to exist.
export default function viewProject({ route }) {
const projID = route.params?.projectID ?? 0; // ERRORS with TypeError: undefined is not an object (evaluating 'route.params')
console.log(projID);
}
I'm using the latest React Native, Navigation etc and TypeScript.
My only success is setting the initialRouteParams on the createStackNavigator that feeds into the drawer, and yet that's only accessible via navigation.state.params:
const screens = {
viewProject: {
screen: viewProject,
navigationOptions: ({ navigation }) => {
return {
headerTitle: () => (
<Header navigation={navigation} title={"View Project"} />
),
};
},
},
};
const viewProjectStack = createStackNavigator(screens, {
initialRouteParams: { projectID: 23 }, // This works, but it's static and I can't change it
});
Thanks in advance.
Stewart
You can pass like this:-
<Button
title="Go to Details"
onPress={() => {
/* 1. Navigate to the Details route with params */
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
}}
/>
and receive like this:-
function DetailsScreen({ route, navigation }) {
/* 2. Get the param */
const { itemId } = route.params;
const { otherParam } = route.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
);
}
source
Hope it helps!!!
I have a custom component that uses Apollo and React-Select and has two mutations (see below). The react-select is multivalue and needs to be custom because I need an "isSelected" checkbox on it. Not shown in this code, but the initial options list is passed in from the parent container.
The parent Select and its mutation work as expected. However, I'm running into a couple of odd problems with the custom MultiValueContainer. The first is that the first time I select any of the check-boxes, I get an error saying that "Can't call setState (or forceUpdate) on an unmounted component." Note below that I have an empty componentWillUnmount function just to see if it gets called, which it doesn't. But (I assume) as a result of that, when the "toggleThing" mutation is called, the state doesn't have the vars necessary to complete the request. The second time I click it works as expected, with the exception of the second issue.
The second issue is that the onCompleted function on the MultiValueContainer mutation never fires, so even though the server is returning the expected data, it never seems to get back to the mutation and therefore never to the component. The onCompleted function on the parent Select works as expected.
Thanks in advance for any insights anyone might have. Perhaps needless to say, I am relatively new to react/apollo/react-select and apologize in advance for any newbie mistakes. Also, I've tried to scrub and simplify the code so apologies also for any renaming mistakes.
const UPDATE_THINGS = gql`
mutation UpdateThings(
$id: ID!
$newThings: [ThingInput]
) {
updateThings(
id: $id
newThings: $newThings
) {
id
}
}
`;
const TOGGLE_THING = gql`
mutation ToggleThing($id: ID!, $isChecked: Boolean) {
toggleThing(
id: $id
isChecked: $isChecked
) {
id
}
}
`;
class ThingList extends Component {
stylesObj = {
multiValue: base => {
return {
...base,
display: 'flex',
alignItems: 'center',
paddingLeft: '10px',
background: 'none',
border: 'none'
};
}
};
constructor(props) {
super(props);
this.state = {
selectedThings: [],
selectedThingId: '',
selectedThingIsChecked: false
};
}
onUpdateComplete = ({ updateThings }) => {
console.log('onUpdateComplete');
console.log('...data', updateThings );
this.setState({ selectedThings: updateThings });
};
onToggleThing = (thingId, isChecked, toggleThing) => {
console.log('onToggleThing, thingId, isChecked');
this.setState(
{
selectedThingId: thingId,
selectedThingIsChecked: isHighPisCheckedoficiency
},
() => toggleThing()
);
};
onToggleThingComplete = ({ onToggleThing }) => {
console.log('onToggleThingComplete ');
console.log('...data', onToggleThing );
this.setState({ selectedThings: onToggleThing });
};
handleChange = (newValue, actionMeta, updateThings) => {
this.setState(
{
selectedThings: newValue
},
() => updateThings()
);
};
isThingSelected = thing=> {
return thing.isSelected;
};
getSelectedThings = selectedThings => {
console.log('getSelectedSkills');
return selectedThings ? selectedThings.filter(obj => obj.isSelected) : [];
};
componentWillUnmount() {
console.log('componentWillUnmount');
}
render() {
const self = this;
const MultiValueContainer = props => {
// console.log('...props', props.data);
return (
<Mutation
mutation={ TOGGLE_THING }
onCompleted={self.onToggleThingComplete}
variables={{
id: self.state.selectedThingId,
isChecked: self.state.selectedThingIsChecked
}}>
{(toggleThing, { data, loading, error }) => {
if (loading) {
return 'Loading...';
}
if (error) {
return `Error!: ${error}`;
}
return (
<div className={'option d-flex align-items-center'}>
<input
type={'checkbox'}
checked={props.data.isChecked}
onChange={evt => {
self.onToggleThing(
props.data.id,
evt.target.checked,
toggleIsHighProficiency
);
}}
/>
<components.MultiValueContainer {...props} />
</div>
);
}}
</Mutation>
);
};
return (
<Mutation
mutation={UPDATE_THINGS}
onCompleted={this.onUpdateComplete}
variables={{ id: this.id, newThings: this.state.selectedThings}}>
{(updateThings, { data, loading, error }) => {
if (loading) {
return 'Loading...';
}
if (error) {
return `Error!: ${error}`;
}
return (
<div>
<Select
options={this.props.selectedThings}
styles={this.stylesObj}
isClearable
isDisabled={this.props.loading}
isLoading={this.props.loading}
defaultValue={this.props.selectedThings.filter(
obj => obj.isSelected
)}
isOptionSelected={this.isOptionSelected}
isMulti={true}
onChange={(newValue, actionMeta) =>
this.handleChange(
newValue,
actionMeta,
updateThings
)
}
components={{
MultiValueContainer
}}
/>
</div>
);
}}
</Mutation>
);
}
}
export default ThingsList;
You are redefining MultiValueContainer on every render, which is not a good practice and may cause unexpected behavior. Try moving it into separate component to see if it helps.